From e7cf2219b6d98e7318b45c55326ccfb6f0bfa9af Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Mon, 26 May 2025 18:50:05 +0300 Subject: [PATCH] docs: document using env vars in plugins (#12618) --- .../admin/environment-variables/page.mdx | 41 + .../learn/fundamentals/admin/tips/page.mdx | 24 +- .../environment-variables/page.mdx | 180 +- www/apps/book/generated/edit-dates.mjs | 6 +- www/apps/book/public/llms-full.txt | 35540 ++++++++-------- www/apps/resources/app/js-sdk/page.mdx | 19 +- www/apps/resources/generated/edit-dates.mjs | 2 +- 7 files changed, 18015 insertions(+), 17797 deletions(-) diff --git a/www/apps/book/app/learn/fundamentals/admin/environment-variables/page.mdx b/www/apps/book/app/learn/fundamentals/admin/environment-variables/page.mdx index 54bdad63ca..679ea170f6 100644 --- a/www/apps/book/app/learn/fundamentals/admin/environment-variables/page.mdx +++ b/www/apps/book/app/learn/fundamentals/admin/environment-variables/page.mdx @@ -14,6 +14,12 @@ To learn how environment variables are generally loaded in Medusa based on your ## How to Set Environment Variables + + +This only applies to customizations in a Medusa project. For plugins, refer to the [Environment Variables in Plugins](#environment-variables-in-plugins) section. + + + The Medusa Admin is built on top of [Vite](https://vite.dev/). To set an environment variable that you want to use in a widget or UI route, prefix the environment variable with `VITE_`. For example: @@ -81,3 +87,38 @@ Learn more about other Vite environment variables in the [Vite documentation](ht When you build the Medusa application, including the Medusa Admin, with the `build` command, the environment variables are inlined into the build. This means that you can't change the environment variables without rebuilding the application. For example, the `VITE_MY_API_KEY` environment variable in the example above will be replaced with the actual value during the build process. + +--- + +## Environment Variables in Plugins + +As explained in the [previous section](#environment-variables-in-production), environment variables are inlined into the build. This presents a limitation for plugins, where you can't use environment variables. + +Instead, only the following global variable is available in plugins: + +- `__BACKEND_URL__`: The URL of the Medusa backend, as set in the [Medusa configurations](../../../configurations/medusa-config/page.mdx#backendurl). +- `__BASE__`: The base path of the Medusa Admin. (For example, `/app`). +- `__STOREFRONT_URL__`: The URL of the Medusa Storefront, as set in the [Medusa configurations](../../../configurations/medusa-config/page.mdx#storefronturl). + +You can use those variables in your Medusa Admin customizations of a plugin. For example: + +```tsx highlights={[["8"]]} +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Container, Heading } from "@medusajs/ui" + +const ProductWidget = () => { + return ( + +
+ Backend URL: {__BACKEND_URL__} +
+
+ ) +} + +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` diff --git a/www/apps/book/app/learn/fundamentals/admin/tips/page.mdx b/www/apps/book/app/learn/fundamentals/admin/tips/page.mdx index bed3c18904..4abca34b99 100644 --- a/www/apps/book/app/learn/fundamentals/admin/tips/page.mdx +++ b/www/apps/book/app/learn/fundamentals/admin/tips/page.mdx @@ -20,7 +20,10 @@ Do not install Tanstack Query as that will cause unexpected errors in your devel First, create the file `src/admin/lib/config.ts` to setup the SDK for use in your customizations: -```ts + + + +```ts title="src/admin/lib/config.ts" import Medusa from "@medusajs/js-sdk" export const sdk = new Medusa({ @@ -32,7 +35,24 @@ export const sdk = new Medusa({ }) ``` -Notice that you use `import.meta.env` to access environment variables in your customizations, as explained in [this chapter](../environment-variables/page.mdx). + + + +```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](../environment-variables/page.mdx) chapter. diff --git a/www/apps/book/app/learn/fundamentals/environment-variables/page.mdx b/www/apps/book/app/learn/fundamentals/environment-variables/page.mdx index 3b02778508..aec961263e 100644 --- a/www/apps/book/app/learn/fundamentals/environment-variables/page.mdx +++ b/www/apps/book/app/learn/fundamentals/environment-variables/page.mdx @@ -107,7 +107,7 @@ To ensure that the correct `.env` file is loaded as shown in the table above, on Since the Medusa Admin is built on top of [Vite](https://vite.dev/), you prefix the environment variables you want to use in a widget or UI route with `VITE_`. Then, you can access or use them with the `import.meta.env` object. -Learn more in [this documentation](../admin/environment-variables/page.mdx). +Learn more in the [Admin Environment Variables](../admin/environment-variables/page.mdx) chapter. --- @@ -124,111 +124,187 @@ You should opt for setting configurations in `medusa-config.ts` where possible. - Environment Variable - Description - Default + + Environment Variable + + + Description + + + Default + - `HOST` - The host to run the Medusa application on. - `localhost` + + `HOST` + + + The host to run the Medusa application on. + + + `localhost` + - `PORT` - The port to run the Medusa application on. - `9000` + + `PORT` + + + The port to run the Medusa application on. + + + `9000` + - `DATABASE_URL` + + `DATABASE_URL` + The URL to connect to the PostgreSQL database. Only used if [projectConfig.databaseUrl](../../configurations/medusa-config/page.mdx#databaseurl) isn't set in `medusa-config.ts`. - `postgres://localhost/medusa-starter-default` + + `postgres://localhost/medusa-starter-default` + - `STORE_CORS` + + `STORE_CORS` + URLs of storefronts that can access the Medusa backend's Store APIs. Only used if [projectConfig.http.storeCors](../../configurations/medusa-config/page.mdx#http-storeCors-1-1) isn't set in `medusa-config.ts`. - `http://localhost:8000` + + `http://localhost:8000` + `ADMIN_CORS` URLs of admin dashboards that can access the Medusa backend's Admin APIs. Only used if [projectConfig.http.adminCors](../../configurations/medusa-config/page.mdx#http-adminCors-1-2) isn't set in `medusa-config.ts`. - `http://localhost:7000,http://localhost:7001,http://localhost:5173` + + `http://localhost:7000,http://localhost:7001,http://localhost:5173` + `AUTH_CORS` URLs of clients that can access the Medusa backend's authentication routes. Only used if [projectConfig.http.authCors](../../configurations/medusa-config/page.mdx#http-authCors-1-0) isn't set in `medusa-config.ts`. - `http://localhost:7000,http://localhost:7001,http://localhost:5173` + + `http://localhost:7000,http://localhost:7001,http://localhost:5173` + `JWT_SECRET` A random string used to create authentication tokens in the http layer. Only used if [projectConfig.http.jwtSecret](../../configurations/medusa-config/page.mdx#http-jwtSecret-1-3) isn't set in `medusa-config.ts`. - \- + + \- + - `COOKIE_SECRET` + + `COOKIE_SECRET` + A random string used to create cookie tokens in the http layer. Only used if [projectConfig.http.cookieSecret](../../configurations/medusa-config/page.mdx#http-cookieSecret-1-5) isn't set in `medusa-config.ts`. - \- + + \- + - `MEDUSA_BACKEND_URL` + + `MEDUSA_BACKEND_URL` + The URL to the Medusa backend. Only used if [admin.backendUrl](../../configurations/medusa-config/page.mdx#backendurl) isn't set in `medusa-config.ts`. - \- - - - `DB_HOST` - The host for the database. It's used when generating migrations for a plugin, and when running integration tests. - `localhost` - - - `DB_USERNAME` - The username for the database. It's used when generating migrations for a plugin, and when running integration tests. - \- - - - `DB_PASSWORD` - The password for the database user. It's used when generating migrations for a plugin, and when running integration tests. - \- - - - `DB_TEMP_NAME` - The database name to create for integration tests. - \- - - - `LOG_LEVEL` - The allowed levels to log. Learn more in [this doumentation](../../debugging-and-testing/logging/page.mdx#log-levels). + \- - `silly` - `LOG_FILE` - The file to save logs in. By default, logs aren't saved in any file. Learn more in [this documentation](../../debugging-and-testing/logging/page.mdx). + `DB_HOST` + + + The host for the database. It's used when generating migrations for a plugin, and when running integration tests. + + + `localhost` - \- - `MEDUSA_DISABLE_TELEMETRY` - Whether to disable analytics data collection. Learn more in [this documentation](../../resources/usage/page.mdx). + `DB_USERNAME` + + + The username for the database. It's used when generating migrations for a plugin, and when running integration tests. + + + \- + + + + + `DB_PASSWORD` + + + The password for the database user. It's used when generating migrations for a plugin, and when running integration tests. + + + \- + + + + + `DB_TEMP_NAME` + + + The database name to create for integration tests. + + + \- + + + + + `LOG_LEVEL` + + + The allowed levels to log. Learn more in the [Logging](../../debugging-and-testing/logging/page.mdx#log-levels) chapter. + + + `silly` + + + + + `LOG_FILE` + + + The file to save logs in. By default, logs aren't saved in any file. Learn more in the [Logging](../../debugging-and-testing/logging/page.mdx) chapter. + + + \- + + + + + `MEDUSA_DISABLE_TELEMETRY` + + + Whether to disable analytics data collection. Learn more in the [Usage](../../resources/usage/page.mdx) chapter. + + + \- - \-
diff --git a/www/apps/book/generated/edit-dates.mjs b/www/apps/book/generated/edit-dates.mjs index 747205b7d5..de8e349463 100644 --- a/www/apps/book/generated/edit-dates.mjs +++ b/www/apps/book/generated/edit-dates.mjs @@ -43,7 +43,7 @@ export const generatedEditDates = { "app/learn/fundamentals/scheduled-jobs/execution-number/page.mdx": "2024-10-21T13:30:21.371Z", "app/learn/fundamentals/api-routes/parameters/page.mdx": "2025-02-14T08:34:03.184Z", "app/learn/fundamentals/api-routes/http-methods/page.mdx": "2024-10-21T13:30:21.367Z", - "app/learn/fundamentals/admin/tips/page.mdx": "2025-03-11T08:54:12.028Z", + "app/learn/fundamentals/admin/tips/page.mdx": "2025-05-26T14:58:56.390Z", "app/learn/fundamentals/api-routes/cors/page.mdx": "2025-03-11T08:54:26.281Z", "app/learn/fundamentals/admin/ui-routes/page.mdx": "2025-02-24T09:35:11.752Z", "app/learn/fundamentals/api-routes/middlewares/page.mdx": "2025-05-09T07:56:04.125Z", @@ -93,7 +93,7 @@ export const generatedEditDates = { "app/learn/introduction/architecture/page.mdx": "2025-05-21T14:31:51.644Z", "app/learn/fundamentals/data-models/infer-type/page.mdx": "2025-03-18T07:41:01.936Z", "app/learn/fundamentals/custom-cli-scripts/seed-data/page.mdx": "2024-12-09T14:38:06.385Z", - "app/learn/fundamentals/environment-variables/page.mdx": "2025-03-11T08:55:03.343Z", + "app/learn/fundamentals/environment-variables/page.mdx": "2025-05-26T15:06:07.800Z", "app/learn/build/page.mdx": "2025-04-25T12:34:33.914Z", "app/learn/deployment/general/page.mdx": "2025-04-17T08:29:09.878Z", "app/learn/fundamentals/workflows/multiple-step-usage/page.mdx": "2024-11-25T16:19:32.169Z", @@ -106,7 +106,7 @@ export const generatedEditDates = { "app/learn/customization/reuse-customizations/page.mdx": "2025-01-22T10:01:57.665Z", "app/learn/update/page.mdx": "2025-01-27T08:45:19.030Z", "app/learn/fundamentals/module-links/query-context/page.mdx": "2025-02-12T16:59:20.963Z", - "app/learn/fundamentals/admin/environment-variables/page.mdx": "2025-03-25T13:32:19.713Z", + "app/learn/fundamentals/admin/environment-variables/page.mdx": "2025-05-26T15:02:25.624Z", "app/learn/fundamentals/api-routes/parse-body/page.mdx": "2025-04-17T08:29:10.145Z", "app/learn/fundamentals/admin/routing/page.mdx": "2025-02-24T09:50:37.495Z", "app/learn/resources/contribution-guidelines/admin-translations/page.mdx": "2025-02-11T16:57:46.726Z", diff --git a/www/apps/book/public/llms-full.txt b/www/apps/book/public/llms-full.txt index da66ebae2f..26f3b0fa7f 100644 --- a/www/apps/book/public/llms-full.txt +++ b/www/apps/book/public/llms-full.txt @@ -345,28 +345,6 @@ Refer to [this documentation](https://docs.medusajs.com/learn/update/index.html. In the next chapters, you'll learn about the architecture of your Medusa application, then learn how to customize your application to build custom features. -# Storefront Development - -The Medusa application is made up of a Node.js server and an admin dashboard. Storefronts are installed, built, and hosted separately from the Medusa application, giving you the flexibility to choose the frontend tech stack that you and your team are proficient in, and implement unique design systems and user experience. - -You can build your storefront from scratch with your preferred tech stack, or start with our Next.js Starter storefront. The Next.js Starter storefront provides rich commerce features and a sleek design. Developers and businesses can use it as-is or build on top of it to tailor it for the business's unique use case, design, and customer experience. - -- [Install Next.js Starter Storefront](https://docs.medusajs.com/resources/nextjs-starter/index.html.md) -- [Build Custom Storefront](https://docs.medusajs.com/resources/storefront-development/index.html.md) - -*** - -## Passing a Publishable API Key in Storefront Requests - -When sending a request to an API route starting with `/store`, you must include a publishable API key in the header of your request. - -A publishable API key sets the scope of your request to one or more sales channels. - -Then, when you retrieve products, only products of those sales channels are retrieved. This also ensures you retrieve correct inventory data, and associate created orders with the scoped sales channel. - -Learn more about passing the publishable API key in [this storefront development guide](https://docs.medusajs.com/resources/storefront-development/publishable-api-keys/index.html.md). - - # Updating Medusa In this chapter, you'll learn about updating your Medusa application and packages. @@ -473,6 +451,72 @@ npm install ``` +# Storefront Development + +The Medusa application is made up of a Node.js server and an admin dashboard. Storefronts are installed, built, and hosted separately from the Medusa application, giving you the flexibility to choose the frontend tech stack that you and your team are proficient in, and implement unique design systems and user experience. + +You can build your storefront from scratch with your preferred tech stack, or start with our Next.js Starter storefront. The Next.js Starter storefront provides rich commerce features and a sleek design. Developers and businesses can use it as-is or build on top of it to tailor it for the business's unique use case, design, and customer experience. + +- [Install Next.js Starter Storefront](https://docs.medusajs.com/resources/nextjs-starter/index.html.md) +- [Build Custom Storefront](https://docs.medusajs.com/resources/storefront-development/index.html.md) + +*** + +## Passing a Publishable API Key in Storefront Requests + +When sending a request to an API route starting with `/store`, you must include a publishable API key in the header of your request. + +A publishable API key sets the scope of your request to one or more sales channels. + +Then, when you retrieve products, only products of those sales channels are retrieved. This also ensures you retrieve correct inventory data, and associate created orders with the scoped sales channel. + +Learn more about passing the publishable API key in [this storefront development guide](https://docs.medusajs.com/resources/storefront-development/publishable-api-keys/index.html.md). + + +# Using TypeScript Aliases + +By default, Medusa doesn't support TypeScript aliases in production. + +If you prefer using TypeScript aliases, install following development dependencies: + +```bash npm2yarn +npm install --save-dev tsc-alias rimraf +``` + +Where `tsc-alias` is a package that resolves TypeScript aliases, and `rimraf` is a package that removes files and directories. + +Then, add a new `resolve:aliases` script to your `package.json` and update the `build` script: + +```json title="package.json" +{ + "scripts": { + // other scripts... + "resolve:aliases": "tsc --showConfig -p tsconfig.json > tsconfig.resolved.json && tsc-alias -p tsconfig.resolved.json && rimraf tsconfig.resolved.json", + "build": "medusa build && npm run resolve:aliases" + } +} +``` + +You can now use TypeScript aliases in your Medusa application. For example, add the following in `tsconfig.json`: + +```json title="tsconfig.json" +{ + "compilerOptions": { + // ... + "paths": { + "@/*": ["./src/*"] + } + } +} +``` + +Now, you can import modules, for example, using TypeScript aliases: + +```ts +import { BrandModuleService } from "@/modules/brand/service" +``` + + # Medusa Application Configuration In this chapter, you'll learn available configurations in the Medusa application. You can change the application's configurations to customize the behavior of the application, its integrated modules and plugins, and more. @@ -1376,48 +1420,350 @@ npx medusa db:migrate ``` -# Using TypeScript Aliases +# Configure Instrumentation -By default, Medusa doesn't support TypeScript aliases in production. +In this chapter, you'll learn about observability in Medusa and how to configure instrumentation with OpenTelemetry. -If you prefer using TypeScript aliases, install following development dependencies: +## Observability with OpenTelemtry + +Medusa uses [OpenTelemetry](https://opentelemetry.io/) for instrumentation and reporting. When configured, it reports traces for: + +- HTTP requests +- Workflow executions +- Query usages +- Database queries and operations + +*** + +## How to Configure Instrumentation in Medusa? + +### Prerequisites + +- [An exporter to visualize your application's traces, such as Zipkin.](https://zipkin.io/pages/quickstart.html) + +### Install Dependencies + +Start by installing the following OpenTelemetry dependencies in your Medusa project: ```bash npm2yarn -npm install --save-dev tsc-alias rimraf +npm install @opentelemetry/sdk-node @opentelemetry/resources @opentelemetry/sdk-trace-node @opentelemetry/instrumentation-pg ``` -Where `tsc-alias` is a package that resolves TypeScript aliases, and `rimraf` is a package that removes files and directories. +Also, install the dependencies relevant for the exporter you use. If you're using Zipkin, install the following dependencies: -Then, add a new `resolve:aliases` script to your `package.json` and update the `build` script: +```bash npm2yarn +npm install @opentelemetry/exporter-zipkin +``` + +### Add instrumentation.ts + +Next, create the file `instrumentation.ts` with the following content: + +```ts title="instrumentation.ts" +import { registerOtel } from "@medusajs/medusa" +import { ZipkinExporter } from "@opentelemetry/exporter-zipkin" + +// If using an exporter other than Zipkin, initialize it here. +const exporter = new ZipkinExporter({ + serviceName: "my-medusa-project", +}) + +export function register() { + registerOtel({ + serviceName: "medusajs", + // pass exporter + exporter, + instrument: { + http: true, + workflows: true, + query: true, + }, + }) +} +``` + +In the `instrumentation.ts` file, you export a `register` function that uses Medusa's `registerOtel` utility function. You also initialize an instance of the exporter, such as Zipkin, and pass it to the `registerOtel` function. + +`registerOtel` accepts an object where you can pass any [NodeSDKConfiguration](https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_sdk_node.NodeSDKConfiguration.html) property along with the following properties: + +The `NodeSDKConfiguration` properties are accepted since Medusa v2.5.1. + +- serviceName: (\`string\`) The name of the service traced. +- exporter: (\[SpanExporter]\(https://open-telemetry.github.io/opentelemetry-js/interfaces/\_opentelemetry\_sdk\_trace\_base.SpanExporter.html)) An instance of an exporter, such as Zipkin. +- instrument: (\`object\`) Options specifying what to trace. + + - http: (\`boolean\`) Whether to trace HTTP requests. + + - query: (\`boolean\`) Whether to trace Query usages. + + - workflows: (\`boolean\`) Whether to trace Workflow executions. + + - db: (\`boolean\`) Whether to trace database queries and operations. +- instrumentations: (\[Instrumentation\[]]\(https://open-telemetry.github.io/opentelemetry-js/interfaces/\_opentelemetry\_instrumentation.Instrumentation.html)) Additional instrumentation options that OpenTelemetry accepts. + +*** + +## Test it Out + +To test it out, start your exporter, such as Zipkin. + +Then, start your Medusa application: + +```bash npm2yarn +npm run dev +``` + +Try to open the Medusa Admin or send a request to an API route. + +If you check traces in your exporter, you'll find new traces reported. + +### Trace Span Names + +Trace span names start with the following keywords based on what it's reporting: + +- `{methodName} {URL}` when reporting HTTP requests, where `{methodName}` is the HTTP method, and `{URL}` is the URL the request is sent to. +- `route:` when reporting route handlers running on an HTTP request. +- `middleware:` when reporting a middleware running on an HTTP request. +- `workflow:` when reporting a workflow execution. +- `step:` when reporting a step in a workflow execution. +- `query.graph:` when reporting Query usages. +- `pg.query:` when reporting database queries and operations. + + +# Logging + +In this chapter, you’ll learn how to use Medusa’s logging utility. + +## Logger Class + +Medusa provides a `Logger` class with advanced logging functionalities. This includes configuring logging levels or saving logs to a file. + +The Medusa application registers the `Logger` class in the Medusa container and each module's container as `logger`. + +*** + +## How to Log a Message + +Resolve the `logger` using the Medusa container to log a message in your resource. + +For example, create the file `src/jobs/log-message.ts` with the following content: + +```ts title="src/jobs/log-message.ts" highlights={highlights} +import { MedusaContainer } from "@medusajs/framework/types" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" + +export default async function myCustomJob( + container: MedusaContainer +) { + const logger = container.resolve(ContainerRegistrationKeys.LOGGER) + + logger.info("I'm using the logger!") +} + +export const config = { + name: "test-logger", + // execute every minute + schedule: "* * * * *", +} +``` + +This creates a scheduled job that resolves the `logger` from the Medusa container and uses it to log a message. + +### Test the Scheduled Job + +To test out the above scheduled job, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +After a minute, you'll see the following message as part of the logged messages: + +```text +info: I'm using the logger! +``` + +*** + +## Log Levels + +The `Logger` class has the following methods: + +- `info`: The message is logged with level `info`. +- `warn`: The message is logged with level `warn`. +- `error`: The message is logged with level `error`. +- `debug`: The message is logged with level `debug`. + +Each of these methods accepts a string parameter to log in the terminal with the associated level. + +*** + +## Logging Configurations + +### Log Level + +The available log levels, from lowest to highest levels, are: + +1. `silly` (default, meaning messages of all levels are logged) +2. `debug` +3. `info` +4. `warn` +5. `error` + +You can change that by setting the `LOG_LEVEL` environment variable to the minimum level you want to be logged. + +For example: + +```bash +LOG_LEVEL=error +``` + +This logs `error` messages only. + +The environment variable must be set as a system environment variable and not in `.env`. + +### Save Logs in a File + +Aside from showing the logs in the terminal, you can save the logs in a file by setting the `LOG_FILE` environment variable to the path of the file relative to the Medusa server’s root directory. + +For example: + +```bash +LOG_FILE=all.log +``` + +Your logs are now saved in the `all.log` file at the root of your Medusa application. + +The environment variable must be set as a system environment variable and not in `.env`. + +*** + +## Show Log with Progress + +The `Logger` class has an `activity` method used to log a message of level `info`. If the Medusa application is running in a development environment, a spinner starts to show the activity's progress. + +For example: + +```ts title="src/jobs/log-message.ts" +import { MedusaContainer } from "@medusajs/framework/types" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" + +export default async function myCustomJob( + container: MedusaContainer +) { + const logger = container.resolve(ContainerRegistrationKeys.LOGGER) + + const activityId = logger.activity("First log message") + + logger.progress(activityId, `Second log message`) + + logger.success(activityId, "Last log message") +} +``` + +The `activity` method returns the ID of the started activity. This ID can then be passed to one of the following methods of the `Logger` class: + +- `progress`: Log a message of level `info` that indicates progress within that same activity. +- `success`: Log a message of level `info` that indicates that the activity has succeeded. This also ends the associated activity. +- `failure`: Log a message of level `error` that indicates that the activity has failed. This also ends the associated activity. + +If you configured the `LOG_LEVEL` environment variable to a level higher than those associated with the above methods, their messages won’t be logged. + + +# Medusa Testing Tools + +In this chapter, you'll learn about Medusa's testing tools and how to install and configure them. + +## @medusajs/test-utils Package + +Medusa provides a Testing Framework to create integration tests for your custom API routes, modules, or other Medusa customizations. + +To use the Testing Framework, install `@medusajs/test-utils` as a `devDependency`: + +```bash npm2yarn +npm install --save-dev @medusajs/test-utils@latest +``` + +*** + +## Install and Configure Jest + +Writing tests with `@medusajs/test-utils`'s tools requires installing and configuring Jest in your project. + +Run the following command to install the required Jest dependencies: + +```bash npm2yarn +npm install --save-dev jest @types/jest @swc/jest +``` + +Then, create the file `jest.config.js` with the following content: + +```js title="jest.config.js" +const { loadEnv } = require("@medusajs/framework/utils") +loadEnv("test", process.cwd()) + +module.exports = { + transform: { + "^.+\\.[jt]s$": [ + "@swc/jest", + { + jsc: { + parser: { syntax: "typescript", decorators: true }, + }, + }, + ], + }, + testEnvironment: "node", + moduleFileExtensions: ["js", "ts", "json"], + modulePathIgnorePatterns: ["dist/"], + setupFiles: ["./integration-tests/setup.js"], +} + +if (process.env.TEST_TYPE === "integration:http") { + module.exports.testMatch = ["**/integration-tests/http/*.spec.[jt]s"] +} else if (process.env.TEST_TYPE === "integration:modules") { + module.exports.testMatch = ["**/src/modules/*/__tests__/**/*.[jt]s"] +} else if (process.env.TEST_TYPE === "unit") { + module.exports.testMatch = ["**/src/**/__tests__/**/*.unit.spec.[jt]s"] +} +``` + +Next, create the `integration-tests/setup.js` file with the following content: + +```js title="integration-tests/setup.js" +const { MetadataStorage } = require("@mikro-orm/core") + +MetadataStorage.clear() +``` + +*** + +## Add Test Commands + +Finally, add the following scripts to `package.json`: ```json title="package.json" -{ - "scripts": { - // other scripts... - "resolve:aliases": "tsc --showConfig -p tsconfig.json > tsconfig.resolved.json && tsc-alias -p tsconfig.resolved.json && rimraf tsconfig.resolved.json", - "build": "medusa build && npm run resolve:aliases" - } -} +"scripts": { + // ... + "test:integration:http": "TEST_TYPE=integration:http NODE_OPTIONS=--experimental-vm-modules jest --silent=false --runInBand --forceExit", + "test:integration:modules": "TEST_TYPE=integration:modules NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit", + "test:unit": "TEST_TYPE=unit NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit" +}, ``` -You can now use TypeScript aliases in your Medusa application. For example, add the following in `tsconfig.json`: +You now have two commands: -```json title="tsconfig.json" -{ - "compilerOptions": { - // ... - "paths": { - "@/*": ["./src/*"] - } - } -} -``` +- `test:integration:http` to run integration tests (for example, for API routes and workflows) available under the `integration-tests/http` directory. +- `test:integration:modules` to run integration tests for modules available in any `__tests__` directory under `src/modules`. +- `test:unit` to run unit tests in any `__tests__` directory under the `src` directory. -Now, you can import modules, for example, using TypeScript aliases: +Medusa's Testing Framework works for integration tests only. You can write unit tests using Jest. -```ts -import { BrandModuleService } from "@/modules/brand/service" -``` +*** + +## Test Tools and Writing Tests + +The next chapters explain how to use the testing tools provided by `@medusajs/test-utils` to write tests. # General Medusa Application Deployment Guide @@ -1728,350 +2074,133 @@ Replace the email `admin-medusa@test.com` and password `supersecret` with the cr You can use these credentials to log into the Medusa Admin dashboard. -# Logging +# Build Custom Features -In this chapter, you’ll learn how to use Medusa’s logging utility. +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. -## Logger Class +By following these guides, you'll add brands to the Medusa application that you can associate with products. -Medusa provides a `Logger` class with advanced logging functionalities. This includes configuring logging levels or saving logs to a file. +To build a custom feature in Medusa, you need three main tools: -The Medusa application registers the `Logger` class in the Medusa container and each module's container as `logger`. +- [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) *** -## How to Log a Message +## Next Chapters: Brand Module Example -Resolve the `logger` using the Medusa container to log a message in your resource. +The next chapters will guide you to: -For example, create the file `src/jobs/log-message.ts` with the following content: +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. -```ts title="src/jobs/log-message.ts" highlights={highlights} -import { MedusaContainer } from "@medusajs/framework/types" -import { ContainerRegistrationKeys } from "@medusajs/framework/utils" -export default async function myCustomJob( - container: MedusaContainer -) { - const logger = container.resolve(ContainerRegistrationKeys.LOGGER) +# Customize Medusa Admin Dashboard - logger.info("I'm using the logger!") -} +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). -export const config = { - name: "test-logger", - // execute every minute - schedule: "* * * * *", -} -``` +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: -This creates a scheduled job that resolves the `logger` from the Medusa container and uses it to log a message. +- 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). -### Test the Scheduled Job - -To test out the above scheduled job, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -After a minute, you'll see the following message as part of the logged messages: - -```text -info: I'm using the logger! -``` +From these customizations, you can send requests to custom API routes, allowing admin users to manage custom resources on the dashboard *** -## Log Levels +## Next Chapters: View Brands in Dashboard -The `Logger` class has the following methods: +In the next chapters, you'll continue with the brands example to: -- `info`: The message is logged with level `info`. -- `warn`: The message is logged with level `warn`. -- `error`: The message is logged with level `error`. -- `debug`: The message is logged with level `debug`. +- 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. -Each of these methods accepts a string parameter to log in the terminal with the associated level. + +# 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. *** -## Logging Configurations +## More Examples in Recipes -### Log Level +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. -The available log levels, from lowest to highest levels, are: -1. `silly` (default, meaning messages of all levels are logged) -2. `debug` -3. `info` -4. `warn` -5. `error` +# Extend Core Commerce Features -You can change that by setting the `LOG_LEVEL` environment variable to the minimum level you want to be logged. +In the upcoming chapters, you'll learn about the concepts and tools to extend Medusa's core commerce features. -For example: +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. -```bash -LOG_LEVEL=error -``` +The Medusa Framework and orchestration tools mitigate these issues while supporting all your customization needs: -This logs `error` messages only. - -The environment variable must be set as a system environment variable and not in `.env`. - -### Save Logs in a File - -Aside from showing the logs in the terminal, you can save the logs in a file by setting the `LOG_FILE` environment variable to the path of the file relative to the Medusa server’s root directory. - -For example: - -```bash -LOG_FILE=all.log -``` - -Your logs are now saved in the `all.log` file at the root of your Medusa application. - -The environment variable must be set as a system environment variable and not in `.env`. +- [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. *** -## Show Log with Progress +## Next Chapters: Link Brands to Products Example -The `Logger` class has an `activity` method used to log a message of level `info`. If the Medusa application is running in a development environment, a spinner starts to show the activity's progress. +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: -For example: - -```ts title="src/jobs/log-message.ts" -import { MedusaContainer } from "@medusajs/framework/types" -import { ContainerRegistrationKeys } from "@medusajs/framework/utils" - -export default async function myCustomJob( - container: MedusaContainer -) { - const logger = container.resolve(ContainerRegistrationKeys.LOGGER) - - const activityId = logger.activity("First log message") - - logger.progress(activityId, `Second log message`) - - logger.success(activityId, "Last log message") -} -``` - -The `activity` method returns the ID of the started activity. This ID can then be passed to one of the following methods of the `Logger` class: - -- `progress`: Log a message of level `info` that indicates progress within that same activity. -- `success`: Log a message of level `info` that indicates that the activity has succeeded. This also ends the associated activity. -- `failure`: Log a message of level `error` that indicates that the activity has failed. This also ends the associated activity. - -If you configured the `LOG_LEVEL` environment variable to a level higher than those associated with the above methods, their messages won’t be logged. +- 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. -# Configure Instrumentation +# Integrate Third-Party Systems -In this chapter, you'll learn about observability in Medusa and how to configure instrumentation with OpenTelemetry. +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. -## Observability with OpenTelemtry +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. -Medusa uses [OpenTelemetry](https://opentelemetry.io/) for instrumentation and reporting. When configured, it reports traces for: +In Medusa, you integrate a third-party system by: -- HTTP requests -- Workflow executions -- Query usages -- Database queries and operations +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. *** -## How to Configure Instrumentation in Medusa? +## Next Chapters: Sync Brands Example -### Prerequisites +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: -- [An exporter to visualize your application's traces, such as Zipkin.](https://zipkin.io/pages/quickstart.html) - -### Install Dependencies - -Start by installing the following OpenTelemetry dependencies in your Medusa project: - -```bash npm2yarn -npm install @opentelemetry/sdk-node @opentelemetry/resources @opentelemetry/sdk-trace-node @opentelemetry/instrumentation-pg -``` - -Also, install the dependencies relevant for the exporter you use. If you're using Zipkin, install the following dependencies: - -```bash npm2yarn -npm install @opentelemetry/exporter-zipkin -``` - -### Add instrumentation.ts - -Next, create the file `instrumentation.ts` with the following content: - -```ts title="instrumentation.ts" -import { registerOtel } from "@medusajs/medusa" -import { ZipkinExporter } from "@opentelemetry/exporter-zipkin" - -// If using an exporter other than Zipkin, initialize it here. -const exporter = new ZipkinExporter({ - serviceName: "my-medusa-project", -}) - -export function register() { - registerOtel({ - serviceName: "medusajs", - // pass exporter - exporter, - instrument: { - http: true, - workflows: true, - query: true, - }, - }) -} -``` - -In the `instrumentation.ts` file, you export a `register` function that uses Medusa's `registerOtel` utility function. You also initialize an instance of the exporter, such as Zipkin, and pass it to the `registerOtel` function. - -`registerOtel` accepts an object where you can pass any [NodeSDKConfiguration](https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_sdk_node.NodeSDKConfiguration.html) property along with the following properties: - -The `NodeSDKConfiguration` properties are accepted since Medusa v2.5.1. - -- serviceName: (\`string\`) The name of the service traced. -- exporter: (\[SpanExporter]\(https://open-telemetry.github.io/opentelemetry-js/interfaces/\_opentelemetry\_sdk\_trace\_base.SpanExporter.html)) An instance of an exporter, such as Zipkin. -- instrument: (\`object\`) Options specifying what to trace. - - - http: (\`boolean\`) Whether to trace HTTP requests. - - - query: (\`boolean\`) Whether to trace Query usages. - - - workflows: (\`boolean\`) Whether to trace Workflow executions. - - - db: (\`boolean\`) Whether to trace database queries and operations. -- instrumentations: (\[Instrumentation\[]]\(https://open-telemetry.github.io/opentelemetry-js/interfaces/\_opentelemetry\_instrumentation.Instrumentation.html)) Additional instrumentation options that OpenTelemetry accepts. - -*** - -## Test it Out - -To test it out, start your exporter, such as Zipkin. - -Then, start your Medusa application: - -```bash npm2yarn -npm run dev -``` - -Try to open the Medusa Admin or send a request to an API route. - -If you check traces in your exporter, you'll find new traces reported. - -### Trace Span Names - -Trace span names start with the following keywords based on what it's reporting: - -- `{methodName} {URL}` when reporting HTTP requests, where `{methodName}` is the HTTP method, and `{URL}` is the URL the request is sent to. -- `route:` when reporting route handlers running on an HTTP request. -- `middleware:` when reporting a middleware running on an HTTP request. -- `workflow:` when reporting a workflow execution. -- `step:` when reporting a step in a workflow execution. -- `query.graph:` when reporting Query usages. -- `pg.query:` when reporting database queries and operations. - - -# Medusa Testing Tools - -In this chapter, you'll learn about Medusa's testing tools and how to install and configure them. - -## @medusajs/test-utils Package - -Medusa provides a Testing Framework to create integration tests for your custom API routes, modules, or other Medusa customizations. - -To use the Testing Framework, install `@medusajs/test-utils` as a `devDependency`: - -```bash npm2yarn -npm install --save-dev @medusajs/test-utils@latest -``` - -*** - -## Install and Configure Jest - -Writing tests with `@medusajs/test-utils`'s tools requires installing and configuring Jest in your project. - -Run the following command to install the required Jest dependencies: - -```bash npm2yarn -npm install --save-dev jest @types/jest @swc/jest -``` - -Then, create the file `jest.config.js` with the following content: - -```js title="jest.config.js" -const { loadEnv } = require("@medusajs/framework/utils") -loadEnv("test", process.cwd()) - -module.exports = { - transform: { - "^.+\\.[jt]s$": [ - "@swc/jest", - { - jsc: { - parser: { syntax: "typescript", decorators: true }, - }, - }, - ], - }, - testEnvironment: "node", - moduleFileExtensions: ["js", "ts", "json"], - modulePathIgnorePatterns: ["dist/"], - setupFiles: ["./integration-tests/setup.js"], -} - -if (process.env.TEST_TYPE === "integration:http") { - module.exports.testMatch = ["**/integration-tests/http/*.spec.[jt]s"] -} else if (process.env.TEST_TYPE === "integration:modules") { - module.exports.testMatch = ["**/src/modules/*/__tests__/**/*.[jt]s"] -} else if (process.env.TEST_TYPE === "unit") { - module.exports.testMatch = ["**/src/**/__tests__/**/*.unit.spec.[jt]s"] -} -``` - -Next, create the `integration-tests/setup.js` file with the following content: - -```js title="integration-tests/setup.js" -const { MetadataStorage } = require("@mikro-orm/core") - -MetadataStorage.clear() -``` - -*** - -## Add Test Commands - -Finally, add the following scripts to `package.json`: - -```json title="package.json" -"scripts": { - // ... - "test:integration:http": "TEST_TYPE=integration:http NODE_OPTIONS=--experimental-vm-modules jest --silent=false --runInBand --forceExit", - "test:integration:modules": "TEST_TYPE=integration:modules NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit", - "test:unit": "TEST_TYPE=unit NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit" -}, -``` - -You now have two commands: - -- `test:integration:http` to run integration tests (for example, for API routes and workflows) available under the `integration-tests/http` directory. -- `test:integration:modules` to run integration tests for modules available in any `__tests__` directory under `src/modules`. -- `test:unit` to run unit tests in any `__tests__` directory under the `src` directory. - -Medusa's Testing Framework works for integration tests only. You can write unit tests using Jest. - -*** - -## Test Tools and Writing Tests - -The next chapters explain how to use the testing tools provided by `@medusajs/test-utils` to write tests. +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 @@ -2249,108 +2378,77 @@ npx medusa exec ./src/scripts/my-script.ts arg1 arg2 ``` -# Data Models +# Environment Variables -In this chapter, you'll learn what a data model is and how to create a data model. +In this chapter, you'll learn how environment variables are loaded in Medusa. -## What is a Data Model? +## System Environment Variables -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 application loads and uses system environment variables. -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. +For example, if you set the `PORT` environment variable to `8000`, the Medusa application runs on that port instead of `9000`. + +In production, you should always use system environment variables that you set through your hosting provider. *** -## How to Create a Data Model +## Environment Variables in .env Files -In a module, you can create a data model in a TypeScript or JavaScript file under the module's `models` directory. +During development, it's easier to set environment variables in a `.env` file in your repository. -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: +Based on your `NODE_ENV` system environment variable, Medusa will try to load environment variables from the following `.env` files: -![Updated directory overview after adding the data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1732806790/Medusa%20Book/blog-dir-overview-1_jfvovj.jpg) +As of [Medusa v2.5.0](https://github.com/medusajs/medusa/releases/tag/v2.5.0), `NODE_ENV` defaults to `production` when using `medusa start`. Otherwise, it defaults to `development`. -```ts title="src/modules/blog/models/post.ts" -import { model } from "@medusajs/framework/utils" +|\`.env\`| +|---|---| +|\`NODE\_ENV\`|\`.env\`| +|\`NODE\_ENV\`|\`.env.production\`| +|\`NODE\_ENV\`|\`.env.staging\`| +|\`NODE\_ENV\`|\`.env.test\`| -const Post = model.define("post", { - id: model.id().primaryKey(), - title: model.text(), -}) +### Set Environment in `loadEnv` -export default Post -``` +In the `medusa-config.ts` file of your Medusa application, you'll find a `loadEnv` function used that accepts `process.env.NODE_ENV` as a first parameter. -You define the data model using the `define` method of the DML. It accepts two parameters: +This function is responsible for loading the correct `.env` file based on the value of `process.env.NODE_ENV`. -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. +To ensure that the correct `.env` file is loaded as shown in the table above, only specify `development`, `production`, `staging` or `test` as the value of `process.env.NODE_ENV` or as the parameter of `loadEnv`. *** -## Generate Migrations +## Environment Variables for Admin Customizations -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. +Since the Medusa Admin is built on top of [Vite](https://vite.dev/), you prefix the environment variables you want to use in a widget or UI route with `VITE_`. Then, you can access or use them with the `import.meta.env` object. -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. +Learn more in the [Admin Environment Variables](https://docs.medusajs.com/learn/fundamentals/admin/environment-variables/index.html.md) chapter. *** -## Manage Data Models +## Predefined Medusa Environment Variables -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. +The Medusa application uses the following predefined environment variables that you can set: -For example, the Blog Module's service would have methods like `retrievePost` and `createPosts`. +You should opt for setting configurations in `medusa-config.ts` where possible. For a full list of Medusa configurations, refer to the [Medusa Configurations chapter](https://docs.medusajs.com/learn/configurations/medusa-config/index.html.md). -Refer to the [Service Factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md) chapter to learn more about how to extend the service factory and manage data models, and refer to the [Service Factory Reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) for the full list of generated methods and how to use them. +|Environment Variable|Description|Default| +|---|---|---| +|\`HOST\`|The host to run the Medusa application on.|\`localhost\`| +|\`PORT\`|The port to run the Medusa application on.|\`9000\`| +|\`DATABASE\_URL\`|The URL to connect to the PostgreSQL database. Only used if |\`postgres://localhost/medusa-starter-default\`| +|\`STORE\_CORS\`|URLs of storefronts that can access the Medusa backend's Store APIs. Only used if |\`http://localhost:8000\`| +||URLs of admin dashboards that can access the Medusa backend's Admin APIs. Only used if |\`http://localhost:7000,http://localhost:7001,http://localhost:5173\`| +||URLs of clients that can access the Medusa backend's authentication routes. Only used if |\`http://localhost:7000,http://localhost:7001,http://localhost:5173\`| +||A random string used to create authentication tokens in the http layer. Only used if |-| +|\`COOKIE\_SECRET\`|A random string used to create cookie tokens in the http layer. Only used if |-| +|\`MEDUSA\_BACKEND\_URL\`|The URL to the Medusa backend. Only used if |-| +|\`DB\_HOST\`|The host for the database. It's used when generating migrations for a plugin, and when running integration tests.|\`localhost\`| +|\`DB\_USERNAME\`|The username for the database. It's used when generating migrations for a plugin, and when running integration tests.|-| +|\`DB\_PASSWORD\`|The password for the database user. It's used when generating migrations for a plugin, and when running integration tests.|-| +|\`DB\_TEMP\_NAME\`|The database name to create for integration tests.|-| +|\`LOG\_LEVEL\`|The allowed levels to log. Learn more in the |\`silly\`| +|\`LOG\_FILE\`|The file to save logs in. By default, logs aren't saved in any file. Learn more in the |-| +|\`MEDUSA\_DISABLE\_TELEMETRY\`|Whether to disable analytics data collection. Learn more in the |-| # Events and Subscribers @@ -2455,83 +2553,108 @@ 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). -# Environment Variables +# Data Models -In this chapter, you'll learn how environment variables are loaded in Medusa. +In this chapter, you'll learn what a data model is and how to create a data model. -## System Environment Variables +## What is a Data Model? -The Medusa application loads and uses system environment variables. +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. -For example, if you set the `PORT` environment variable to `8000`, the Medusa application runs on that port instead of `9000`. - -In production, you should always use system environment variables that you set through your hosting provider. +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. *** -## Environment Variables in .env Files +## How to Create a Data Model -During development, it's easier to set environment variables in a `.env` file in your repository. +In a module, you can create a data model in a TypeScript or JavaScript file under the module's `models` directory. -Based on your `NODE_ENV` system environment variable, Medusa will try to load environment variables from the following `.env` files: +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: -As of [Medusa v2.5.0](https://github.com/medusajs/medusa/releases/tag/v2.5.0), `NODE_ENV` defaults to `production` when using `medusa start`. Otherwise, it defaults to `development`. +![Updated directory overview after adding the data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1732806790/Medusa%20Book/blog-dir-overview-1_jfvovj.jpg) -|\`.env\`| -|---|---| -|\`NODE\_ENV\`|\`.env\`| -|\`NODE\_ENV\`|\`.env.production\`| -|\`NODE\_ENV\`|\`.env.staging\`| -|\`NODE\_ENV\`|\`.env.test\`| +```ts title="src/modules/blog/models/post.ts" +import { model } from "@medusajs/framework/utils" -### Set Environment in `loadEnv` +const Post = model.define("post", { + id: model.id().primaryKey(), + title: model.text(), +}) -In the `medusa-config.ts` file of your Medusa application, you'll find a `loadEnv` function used that accepts `process.env.NODE_ENV` as a first parameter. +export default Post +``` -This function is responsible for loading the correct `.env` file based on the value of `process.env.NODE_ENV`. +You define the data model using the `define` method of the DML. It accepts two parameters: -To ensure that the correct `.env` file is loaded as shown in the table above, only specify `development`, `production`, `staging` or `test` as the value of `process.env.NODE_ENV` or as the parameter of `loadEnv`. +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. *** -## Environment Variables for Admin Customizations +## Generate Migrations -Since the Medusa Admin is built on top of [Vite](https://vite.dev/), you prefix the environment variables you want to use in a widget or UI route with `VITE_`. Then, you can access or use them with the `import.meta.env` object. +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. -Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/admin/environment-variables/index.html.md). +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. *** -## Predefined Medusa Environment Variables +## Manage Data Models -The Medusa application uses the following predefined environment variables that you can set: +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. -You should opt for setting configurations in `medusa-config.ts` where possible. For a full list of Medusa configurations, refer to the [Medusa Configurations chapter](https://docs.medusajs.com/learn/configurations/medusa-config/index.html.md). +For example, the Blog Module's service would have methods like `retrievePost` and `createPosts`. -|Environment Variable|Description|Default| -|---|---|---|---|---| -| -| -| -| -||The URL to connect to the PostgreSQL database. Only used if || -||URLs of storefronts that can access the Medusa backend's Store APIs. Only used if || -||URLs of admin dashboards that can access the Medusa backend's Admin APIs. Only used if || -||URLs of clients that can access the Medusa backend's authentication routes. Only used if || -||A random string used to create authentication tokens in the http layer. Only used if || -||A random string used to create cookie tokens in the http layer. Only used if || -||The URL to the Medusa backend. Only used if || -| -| -| -| -| -| -| -| -||The allowed levels to log. Learn more in || -||The file to save logs in. By default, logs aren't saved in any file. Learn more in || -||Whether to disable analytics data collection. Learn more in || +Refer to the [Service Factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md) chapter to learn more about how to extend the service factory and manage data models, and refer to the [Service Factory Reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) for the full list of generated methods and how to use them. # Framework Overview @@ -3669,6 +3792,138 @@ npx medusa db:migrate ``` +# Plugins + +In this chapter, you'll learn what a plugin is in Medusa. + +Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). + +## What is a Plugin? + +A plugin is a package of reusable Medusa customizations that you can install in any Medusa application. The supported customizations are [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), [API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md), [Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), [Workflow Hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md), [Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md), [Subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md), [Scheduled Jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md), and [Admin Extensions](https://docs.medusajs.com/learn/fundamentals/admin/index.html.md). + +Plugins allow you to reuse your Medusa customizations across multiple projects or share them with the community. They can be published to npm and installed in any Medusa project. + +![Diagram showcasing a wishlist plugin installed in a Medusa application](https://res.cloudinary.com/dza7lstvk/image/upload/v1737540762/Medusa%20Book/plugin-diagram_oepiis.jpg) + +Learn how to create a wishlist plugin in [this guide](https://docs.medusajs.com/resources/plugins/guides/wishlist/index.html.md). + +*** + +## Plugin vs Module + +A [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) is an isolated package related to a single domain or functionality, such as product reviews or integrating a Content Management System. A module can't access any resources in the Medusa application that are outside its codebase. + +A plugin, on the other hand, can contain multiple Medusa customizations, including modules. Your plugin can define a module, then build flows around it. + +For example, in a plugin, you can define a module that integrates a third-party service, then add a workflow that uses the module when a certain event occurs to sync data to that service. + +- You want to reuse your Medusa customizations across multiple projects. +- You want to share your Medusa customizations with the community. + +- You want to build a custom feature related to a single domain or integrate a third-party service. Instead, use a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). You can wrap that module in a plugin if it's used in other customizations, such as if it has a module link or it's used in a workflow. + +*** + +## How to Create a Plugin? + +The next chapter explains how you can create and publish a plugin. + + +# 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. @@ -3969,161 +4224,170 @@ This will create a post and return it in the response: You can also execute the workflow from a [subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) when an event occurs, or from a [scheduled job](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) to run it at a specified interval. -# Plugins +# Medusa's Architecture -In this chapter, you'll learn what a plugin is in Medusa. +In this chapter, you'll learn about the architectural layers in Medusa. -Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). +Find the full architectural diagram at the [end of this chapter](#full-diagram-of-medusas-architecture). -## What is a Plugin? +## HTTP, Workflow, and Module Layers -A plugin is a package of reusable Medusa customizations that you can install in any Medusa application. The supported customizations are [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), [API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md), [Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), [Workflow Hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md), [Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md), [Subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md), [Scheduled Jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md), and [Admin Extensions](https://docs.medusajs.com/learn/fundamentals/admin/index.html.md). +Medusa is a headless commerce platform. So, storefronts, admin dashboards, and other clients consume Medusa's functionalities through its API routes. -Plugins allow you to reuse your Medusa customizations across multiple projects or share them with the community. They can be published to npm and installed in any Medusa project. +In a common Medusa application, requests go through four layers in the stack. In order of entry, those are: -![Diagram showcasing a wishlist plugin installed in a Medusa application](https://res.cloudinary.com/dza7lstvk/image/upload/v1737540762/Medusa%20Book/plugin-diagram_oepiis.jpg) +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. -Learn how to create a wishlist plugin in [this guide](https://docs.medusajs.com/resources/plugins/guides/wishlist/index.html.md). +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) *** -## Plugin vs Module +## Database Layer -A [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) is an isolated package related to a single domain or functionality, such as product reviews or integrating a Content Management System. A module can't access any resources in the Medusa application that are outside its codebase. +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. -A plugin, on the other hand, can contain multiple Medusa customizations, including modules. Your plugin can define a module, then build flows around it. +Modules can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). -For example, in a plugin, you can define a module that integrates a third-party service, then add a workflow that uses the module when a certain event occurs to sync data to that service. - -- You want to reuse your Medusa customizations across multiple projects. -- You want to share your Medusa customizations with the community. - -- You want to build a custom feature related to a single domain or integrate a third-party service. Instead, use a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). You can wrap that module in a plugin if it's used in other customizations, such as if it has a module link or it's used in a workflow. +![This diagram illustrates how modules connect to the database.](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175379/Medusa%20Book/db-layer_pi7tix.jpg) *** -## How to Create a Plugin? +## Third-Party Integrations Layer -The next chapter explains how you can create and publish a plugin. +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). -# Scheduled Jobs +### Commerce Modules -In this chapter, you’ll learn about scheduled jobs and how to use them. +[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. -## What is a Scheduled Job? +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. -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. +You can replace any of the third-party services mentioned above to build your preferred commerce ecosystem. -In other commerce platforms, this feature isn't natively supported. Instead, you have to setup a separate application to execute cron jobs, which adds complexity as to how you expose this task to be executed in a cron job, or how do you debug it when it's not running within the platform's tooling. +![Diagram illustrating the Commerce Modules integration to third-party services](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175357/Medusa%20Book/service-commerce_qcbdsl.jpg) -Medusa removes this overhead by supporting this feature natively with scheduled jobs. A scheduled job is an asynchronous function that the Medusa application runs at the interval you specify during the Medusa application's runtime. Your efforts are only spent on implementing the functionality performed by the job, such as syncing products to an ERP. +### Infrastructure Modules -- 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. +[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) *** -## How to Create a Scheduled Job? +## Full Diagram of Medusa's Architecture -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. +The following diagram illustrates Medusa's architecture including all its layers. -For example, create the file `src/jobs/hello-world.ts` with the following content: +![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) -![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" +# Worker Mode of Medusa Instance -export default async function greetingJob(container: MedusaContainer) { - const logger = container.resolve("logger") +In this chapter, you'll learn about the different modes of running a Medusa instance and how to configure the mode. - logger.info("Greeting!") -} +## What is Worker Mode? -export const config = { - name: "greeting-every-minute", - schedule: "* * * * *", -} +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 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 set the worker mode configuration to the `process.env.WORKER_MODE` environment variable and set a default value of `shared`. -You also export a `config` object that has the following properties: +Then, in the deployed server Medusa instance, set `WORKER_MODE` to `server`, and in the worker Medusa instance, set `WORKER_MODE` to `worker`: -- `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: +### Server Medusa Instance ```bash -info: Greeting! +WORKER_MODE=server ``` -*** +### Worker Medusa Instance -## 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 * * *", -} +```bash +WORKER_MODE=worker ``` -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. +### Disable Admin in Worker Mode -The next time you start the Medusa application, it will run this job every day at midnight. +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: -# Build Custom Features +```ts title="medusa-config.ts" +module.exports = defineConfig({ + admin: { + disable: process.env.ADMIN_DISABLED === "true" || + false, + }, + // ... +}) +``` -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. +Similar to before, you set the value in an environment variable, allowing you to enable or disable the admin interface based on the environment. -By following these guides, you'll add brands to the Medusa application that you can associate with products. +Then, in the deployed server Medusa instance, set `ADMIN_DISABLED` to `false`, and in the worker Medusa instance, set `ADMIN_DISABLED` to `true`: -To build a custom feature in Medusa, you need three main tools: +### Server Medusa Instance -- [Module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md): a package with commerce logic for a single domain. It defines new tables to add to the database, and a class of methods to manage these tables. -- [Workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md): a tool to perform an operation comprising multiple steps with built-in rollback and retry mechanisms. -- [API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md): a REST endpoint that exposes commerce features to clients, such as the admin dashboard or a storefront. The API route executes a workflow that implements the commerce feature using modules. +```bash +ADMIN_DISABLED=false +``` -![Diagram showcasing the flow of a custom developed feature](https://res.cloudinary.com/dza7lstvk/image/upload/v1725867628/Medusa%20Book/custom-development_nofvp6.jpg) +### Worker Medusa Instance -*** - -## 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. +```bash +ADMIN_DISABLED=true +``` # Workflows @@ -4380,276 +4644,6 @@ 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. -# 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. - - -# 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. - - -# 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). - - -# 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) - - -# 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 -``` - - # 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. @@ -4740,51 +4734,6 @@ MEDUSA_FF_ANALYTICS=false ``` -# 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", -}) -``` - - # 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. @@ -4867,6 +4816,214 @@ To manage that database, such as changing its name or perform operations on it i The next chapters provide examples of writing integration tests for API routes and workflows. +# Guide: Create Brand API Route + +In the previous two chapters, you created a [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) that added the concepts of brands to your application, then created a [workflow to create a brand](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md). In this chapter, you'll expose an API route that allows admin users to create a brand using the workflow from the previous chapter. + +An API Route is an endpoint that acts as an entry point for other clients to interact with your Medusa customizations, such as the admin dashboard, storefronts, or third-party systems. + +The Medusa core application provides a set of [admin](https://docs.medusajs.com/api/admin) and [store](https://docs.medusajs.com/api/store) API routes out-of-the-box. You can also create custom API routes to expose your custom functionalities. + +### Prerequisites + +- [createBrandWorkflow](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md) + +## 1. Create the API Route + +You create an API route in a `route.{ts,js}` file under a sub-directory of the `src/api` directory. The file exports API Route handler functions for at least one HTTP method (`GET`, `POST`, `DELETE`, etc…). + +Learn more about API routes [in this guide](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md). + +The route's path is the path of `route.{ts,js}` relative to `src/api`. So, to create the API route at `/admin/brands`, create the file `src/api/admin/brands/route.ts` with the following content: + +![Directory structure of the Medusa application after adding the route](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869882/Medusa%20Book/brand-route-dir-overview-2_hjqlnf.jpg) + +```ts title="src/api/admin/brands/route.ts" +import { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { + createBrandWorkflow, +} from "../../../workflows/create-brand" + +type PostAdminCreateBrandType = { + name: string +} + +export const POST = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const { result } = await createBrandWorkflow(req.scope) + .run({ + input: req.validatedBody, + }) + + res.json({ brand: result }) +} +``` + +You export a route handler function with its name (`POST`) being the HTTP method of the API route you're exposing. + +The function receives two parameters: a `MedusaRequest` object to access request details, and `MedusaResponse` object to return or manipulate the response. The `MedusaRequest` object's `scope` property is the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) that holds Framework tools and custom and core modules' services. + +`MedusaRequest` accepts the request body's type as a type argument. + +In the API route's handler, you execute the `createBrandWorkflow` by invoking it and passing the Medusa container `req.scope` as a parameter, then invoking its `run` method. You pass the workflow's input in the `input` property of the `run` method's parameter. You pass the request body's parameters using the `validatedBody` property of `MedusaRequest`. + +You return a JSON response with the created brand using the `res.json` method. + +*** + +## 2. Create Validation Schema + +The API route you created accepts the brand's name in the request body. So, you'll create a schema used to validate incoming request body parameters. + +Medusa uses [Zod](https://zod.dev/) to create validation schemas. These schemas are then used to validate incoming request bodies or query parameters. + +Learn more about API route validation in [this chapter](https://docs.medusajs.com/learn/fundamentals/api-routes/validation/index.html.md). + +You create a validation schema in a TypeScript or JavaScript file under a sub-directory of the `src/api` directory. So, create the file `src/api/admin/brands/validators.ts` with the following content: + +![Directory structure of Medusa application after adding validators file](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869806/Medusa%20Book/brand-route-dir-overview-1_yfyjss.jpg) + +```ts title="src/api/admin/brands/validators.ts" +import { z } from "zod" + +export const PostAdminCreateBrand = z.object({ + name: z.string(), +}) +``` + +You export a validation schema that expects in the request body an object having a `name` property whose value is a string. + +You can then replace `PostAdminCreateBrandType` in `src/api/admin/brands/route.ts` with the following: + +```ts title="src/api/admin/brands/route.ts" +// ... +import { z } from "zod" +import { PostAdminCreateBrand } from "./validators" + +type PostAdminCreateBrandType = z.infer + +// ... +``` + +*** + +## 3. Add Validation Middleware + +A middleware is a function executed before the route handler when a request is sent to an API Route. It's useful to guard API routes, parse custom request body types, and apply validation on an API route. + +Learn more about middlewares in [this chapter](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md). + +Medusa provides a `validateAndTransformBody` middleware that accepts a Zod validation schema and returns a response error if a request is sent with body parameters that don't satisfy the validation schema. + +Middlewares are defined in the special file `src/api/middlewares.ts`. So, to add the validation middleware on the API route you created in the previous step, create the file `src/api/middlewares.ts` with the following content: + +![Directory structure of the Medusa application after adding the middleware](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869977/Medusa%20Book/brand-route-dir-overview-3_kcx511.jpg) + +```ts title="src/api/middlewares.ts" +import { + defineMiddlewares, + validateAndTransformBody, +} from "@medusajs/framework/http" +import { PostAdminCreateBrand } from "./admin/brands/validators" + +export default defineMiddlewares({ + routes: [ + { + matcher: "/admin/brands", + method: "POST", + middlewares: [ + validateAndTransformBody(PostAdminCreateBrand), + ], + }, + ], +}) +``` + +You define the middlewares using the `defineMiddlewares` function and export its returned value. The function accepts an object having a `routes` property, which is an array of middleware objects. + +In the middleware object, you define three properties: + +- `matcher`: a string or regular expression indicating the API route path to apply the middleware on. You pass the create brand's route `/admin/brand`. +- `method`: The HTTP method to restrict the middleware to, which is `POST`. +- `middlewares`: An array of middlewares to apply on the route. You pass the `validateAndTransformBody` middleware, passing it the Zod schema you created earlier. + +The Medusa application will now validate the body parameters of `POST` requests sent to `/admin/brands` to ensure they match the Zod validation schema. If not, an error is returned in the response specifying the issues to fix in the request body. + +*** + +## Test API Route + +To test out the API route, start the Medusa application with the following command: + +```bash npm2yarn +npm run dev +``` + +Since the `/admin/brands` API route has a `/admin` prefix, it's only accessible by authenticated admin users. + +So, to retrieve an authenticated token of your admin user, send a `POST` request to the `/auth/user/emailpass` API Route: + +```bash +curl -X POST 'http://localhost:9000/auth/user/emailpass' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "email": "admin@medusa-test.com", + "password": "supersecret" +}' +``` + +Make sure to replace the email and password with your admin user's credentials. + +Don't have an admin user? Refer to [this guide](https://docs.medusajs.com/learn/installation#create-medusa-admin-user/index.html.md). + +Then, send a `POST` request to `/admin/brands`, passing the token received from the previous request in the `Authorization` header: + +```bash +curl -X POST 'http://localhost:9000/admin/brands' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: Bearer {token}' \ +--data '{ + "name": "Acme" +}' +``` + +This returns the created brand in the response: + +```json title="Example Response" +{ + "brand": { + "id": "01J7AX9ES4X113HKY6C681KDZJ", + "name": "Acme", + "created_at": "2024-09-09T08:09:34.244Z", + "updated_at": "2024-09-09T08:09:34.244Z" + } +} +``` + +*** + +## Summary + +By following the previous example chapters, you implemented a custom feature that allows admin users to create a brand. You did that by: + +1. Creating a module that defines and manages a `brand` table in the database. +2. Creating a workflow that uses the module's service to create a brand record, and implements the compensation logic to delete that brand in case an error occurs. +3. Creating an API route that allows admin users to create a brand. + +*** + +## Next Steps: Associate Brand with Product + +Now that you have brands in your Medusa application, you want to associate a brand with a product, which is defined in the [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md). + +In the next chapters, you'll learn how to build associations between data models defined in different modules. + + # Write Tests for Modules In this chapter, you'll learn about `moduleIntegrationTestRunner` from Medusa's Testing Framework and how to use it to write integration tests for a module's main service. @@ -4986,6 +5143,1839 @@ 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. @@ -4994,6 +6984,8 @@ To learn how environment variables are generally loaded in Medusa based on your ## How to Set Environment Variables +This only applies to customizations in a Medusa project. For plugins, refer to the [Environment Variables in Plugins](#environment-variables-in-plugins) section. + The Medusa Admin is built on top of [Vite](https://vite.dev/). To set an environment variable that you want to use in a widget or UI route, prefix the environment variable with `VITE_`. For example: @@ -5062,6 +7054,86 @@ When you build the Medusa application, including the Medusa Admin, with the `bui For example, the `VITE_MY_API_KEY` environment variable in the example above will be replaced with the actual value during the build process. +*** + +## Environment Variables in Plugins + +As explained in the [previous section](#environment-variables-in-production), environment variables are inlined into the build. This presents a limitation for plugins, where you can't use environment variables. + +Instead, only the following global variable is available in plugins: + +- `__BACKEND_URL__`: The URL of the Medusa backend, as set in the [Medusa configurations](https://docs.medusajs.com/learn/configurations/medusa-config#backendurl/index.html.md). +- `__BASE__`: The base path of the Medusa Admin. (For example, `/app`). +- `__STOREFRONT_URL__`: The URL of the Medusa Storefront, as set in the [Medusa configurations](https://docs.medusajs.com/learn/configurations/medusa-config#storefronturl/index.html.md). + +You can use those variables in your Medusa Admin customizations of a plugin. For example: + +```tsx highlights={[["8"]]} +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Container, Heading } from "@medusajs/ui" + +const ProductWidget = () => { + return ( + +
+ Backend URL: {__BACKEND_URL__} +
+
+ ) +} + +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +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 @@ -5216,6 +7288,165 @@ 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 + +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. + + # Admin Development Tips In this chapter, you'll find some tips for your admin development. @@ -5228,7 +7459,9 @@ Do not install Tanstack Query as that will cause unexpected errors in your devel First, create the file `src/admin/lib/config.ts` to setup the SDK for use in your customizations: -```ts +### Medusa Project + +```ts title="src/admin/lib/config.ts" import Medusa from "@medusajs/js-sdk" export const sdk = new Medusa({ @@ -5240,7 +7473,20 @@ export const sdk = new Medusa({ }) ``` -Notice that you use `import.meta.env` to access environment variables in your customizations, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/admin/environment-variables/index.html.md). +### 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). @@ -5347,125 +7593,6 @@ The Medusa Admin dashboard can be displayed in languages other than English, whi Learn how to add a new language translation for the Medusa Admin in [this guide](https://docs.medusajs.com/learn/resources/contribution-guidelines/admin-translations/index.html.md). -# Admin Widgets - -In this chapter, you’ll learn more about widgets and how to use them. - -## What is an Admin Widget? - -The Medusa Admin dashboard's pages are customizable to insert widgets of custom content in pre-defined injection zones. You create these widgets as React components that allow admin users to perform custom actions. - -For example, you can add a widget on the product details page that allow admin users to sync products to a third-party service. - -*** - -## How to Create a Widget? - -### Prerequisites - -- [Medusa application installed](https://docs.medusajs.com/learn/installation/index.html.md) - -You create a widget in a `.tsx` file under the `src/admin/widgets` directory. The file’s default export must be the widget, which is the React component that renders the custom content. The file must also export the widget’s configurations indicating where to insert the widget. - -For example, create the file `src/admin/widgets/product-widget.tsx` with the following content: - -![Example of widget file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732867137/Medusa%20Book/widget-dir-overview_dqsbct.jpg) - -```tsx title="src/admin/widgets/product-widget.tsx" highlights={widgetHighlights} -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Container, Heading } from "@medusajs/ui" - -// The widget -const ProductWidget = () => { - return ( - -
- Product Widget -
-
- ) -} - -// The widget's configurations -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) - -export default ProductWidget -``` - -You export the `ProductWidget` component, which shows the heading `Product Widget`. In the widget, you use [Medusa UI](https://docs.medusajs.com/ui/index.html.md), a package that Medusa maintains to allow you to customize the dashboard with the same components used to build it. - -To export the widget's configurations, you use `defineWidgetConfig` from the Admin Extension SDK. It accepts as a parameter an object with the `zone` property, whose value is a string or an array of strings, each being the name of the zone to inject the widget into. - -In the example above, the widget is injected at the top of a product’s details. - -The widget component must be created as an arrow function. - -### Test the Widget - -To test out the widget, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Then, open a product’s details page. You’ll find your custom widget at the top of the page. - -*** - -## Props Passed in Detail Pages - -Widgets that are injected into a details page receive a `data` prop, which is the main data of the details page. - -For example, a widget injected into the `product.details.before` zone receives the product's details in the `data` prop: - -```tsx title="src/admin/widgets/product-widget.tsx" highlights={detailHighlights} -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Container, Heading } from "@medusajs/ui" -import { - DetailWidgetProps, - AdminProduct, -} from "@medusajs/framework/types" - -// The widget -const ProductWidget = ({ - data, -}: DetailWidgetProps) => { - return ( - -
- - Product Widget {data.title} - -
-
- ) -} - -// The widget's configurations -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) - -export default ProductWidget -``` - -The props type is `DetailWidgetProps`, and it accepts as a type argument the expected type of `data`. For the product details page, it's `AdminProduct`. - -*** - -## Injection Zone - -Refer to [this reference](https://docs.medusajs.com/resources/admin-widget-injection-zones/index.html.md) for the full list of injection zones and their props. - -*** - -## Admin Components List - -To build admin customizations that match the Medusa Admin's designs and layouts, refer to [this guide](https://docs.medusajs.com/resources/admin-components/index.html.md) to find common components. - - # Admin UI Routes In this chapter, you’ll learn how to create a UI route in the admin dashboard. @@ -5702,6 +7829,232 @@ 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 + +In this chapter, you’ll learn more about widgets and how to use them. + +## What is an Admin Widget? + +The Medusa Admin dashboard's pages are customizable to insert widgets of custom content in pre-defined injection zones. You create these widgets as React components that allow admin users to perform custom actions. + +For example, you can add a widget on the product details page that allow admin users to sync products to a third-party service. + +*** + +## How to Create a Widget? + +### Prerequisites + +- [Medusa application installed](https://docs.medusajs.com/learn/installation/index.html.md) + +You create a widget in a `.tsx` file under the `src/admin/widgets` directory. The file’s default export must be the widget, which is the React component that renders the custom content. The file must also export the widget’s configurations indicating where to insert the widget. + +For example, create the file `src/admin/widgets/product-widget.tsx` with the following content: + +![Example of widget file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732867137/Medusa%20Book/widget-dir-overview_dqsbct.jpg) + +```tsx title="src/admin/widgets/product-widget.tsx" highlights={widgetHighlights} +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Container, Heading } from "@medusajs/ui" + +// The widget +const ProductWidget = () => { + return ( + +
+ Product Widget +
+
+ ) +} + +// The widget's configurations +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` + +You export the `ProductWidget` component, which shows the heading `Product Widget`. In the widget, you use [Medusa UI](https://docs.medusajs.com/ui/index.html.md), a package that Medusa maintains to allow you to customize the dashboard with the same components used to build it. + +To export the widget's configurations, you use `defineWidgetConfig` from the Admin Extension SDK. It accepts as a parameter an object with the `zone` property, whose value is a string or an array of strings, each being the name of the zone to inject the widget into. + +In the example above, the widget is injected at the top of a product’s details. + +The widget component must be created as an arrow function. + +### Test the Widget + +To test out the widget, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +Then, open a product’s details page. You’ll find your custom widget at the top of the page. + +*** + +## Props Passed in Detail Pages + +Widgets that are injected into a details page receive a `data` prop, which is the main data of the details page. + +For example, a widget injected into the `product.details.before` zone receives the product's details in the `data` prop: + +```tsx title="src/admin/widgets/product-widget.tsx" highlights={detailHighlights} +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Container, Heading } from "@medusajs/ui" +import { + DetailWidgetProps, + AdminProduct, +} from "@medusajs/framework/types" + +// The widget +const ProductWidget = ({ + data, +}: DetailWidgetProps) => { + return ( + +
+ + Product Widget {data.title} + +
+
+ ) +} + +// The widget's configurations +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` + +The props type is `DetailWidgetProps`, and it accepts as a type argument the expected type of `data`. For the product details page, it's `AdminProduct`. + +*** + +## Injection Zone + +Refer to [this reference](https://docs.medusajs.com/resources/admin-widget-injection-zones/index.html.md) for the full list of injection zones and their props. + +*** + +## Admin Components List + +To build admin customizations that match the Medusa Admin's designs and layouts, refer to [this guide](https://docs.medusajs.com/resources/admin-components/index.html.md) to find common components. + + +# 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. + + # Pass Additional Data to Medusa's API Route In this chapter, you'll learn how to pass additional data in requests to Medusa's API Route. @@ -6056,111 +8409,226 @@ This adds two API Routes: - A `POST` route at `http://localhost:9000/hello-world`. -# Throwing and Handling Errors +# Override API Routes -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. +In this chapter, you'll learn the approach recommended when you need to override an existing API route in Medusa. -## Throw MedusaError +## Approaches to Consider Before Overriding API Routes -When throwing an error in your API routes, middlewares, workflows, or any customization, throw a `MedusaError` from the Medusa Framework. +While building customizations in your Medusa application, you may need to make changes to existing API routes for your business use case. -The Medusa application's API route error handler then wraps your thrown error in a uniform object and returns it in the response. +Medusa provides the following approaches to customize API routes: -For example: +|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.| -```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\`| +If the above approaches do not meet your needs, you can consider the approaches mentioned in the rest of this chapter. *** -## Override Error Handler +## Replicate, Don't Override API Routes -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. +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. -This error handler will also be used for errors thrown in Medusa's API routes and resources. +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. -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" +## 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({ - errorHandler: ( - error: MedusaError | any, - req: MedusaRequest, - res: MedusaResponse, - next: MedusaNextFunction - ) => { - res.status(400).json({ - error: "Something happened.", - }) - }, + routes: [ + { + method: ["POST"], + bodyParser: { preserveRawBody: true }, + matcher: "/custom", + }, + ], }) ``` -The `errorHandler` property's value is a function that accepts four parameters: +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. -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. +Learn more about [middlewares](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md). -This example overrides Medusa's default error handler with a handler that always returns a `400` status code with the same message. +You 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 @@ -6543,57 +9011,6 @@ If you need to change the middlewares applied to a route, you can create a custo Learn more in the [Override API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/override/index.html.md) chapter. -# 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 | - - # API Route Parameters In this chapter, you’ll learn about path, query, and request body parameters. @@ -6739,177 +9156,6 @@ You can apply validation rules on received body parameters to ensure they match Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation#how-to-validate-request-body/index.html.md). -# Configure Request Body Parser - -In this chapter, you'll learn how to configure the request body parser for your API routes. - -## Default Body Parser Configuration - -The Medusa application configures the body parser by default to parse JSON, URL-encoded, and text request content types. You can parse other data types by adding the relevant [Express middleware](https://expressjs.com/en/guide/using-middleware.html) or preserve the raw body data by configuring the body parser, which is useful for webhook requests. - -This chapter shares some examples of configuring the body parser for different data types or use cases. - -*** - -## Preserve Raw Body Data for Webhooks - -If your API route receives webhook requests, you might want to preserve the raw body data. To do this, you can configure the body parser to parse the raw body data and store it in the `req.rawBody` property. - -To do that, create the file `src/api/middlewares.ts` with the following content: - -```ts title="src/api/middlewares.ts" highlights={preserveHighlights} -import { defineMiddlewares } from "@medusajs/framework/http" - -export default defineMiddlewares({ - routes: [ - { - method: ["POST"], - bodyParser: { preserveRawBody: true }, - matcher: "/custom", - }, - ], -}) -``` - -The middleware route object passed to `routes` accepts a `bodyParser` property whose value is an object of configuration for the default body parser. By enabling the `preserveRawBody` property, the raw body data is preserved and stored in the `req.rawBody` property. - -Learn more about [middlewares](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md). - -You can then access the raw body data in your API route handler: - -```ts title="src/api/custom/route.ts" -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" - -export async function POST( - req: MedusaRequest, - res: MedusaResponse -) { - console.log(req.rawBody) - - // TODO use raw body -} -``` - -*** - -## Configure Request Body Size Limit - -By default, the body parser limits the request body size to `100kb`. If a request body exceeds that size, the Medusa application throws an error. - -You can configure the body parser to accept larger request bodies by setting the `sizeLimit` property of the `bodyParser` object in a middleware route object. For example: - -```ts title="src/api/middlewares.ts" highlights={sizeLimitHighlights} -import { defineMiddlewares } from "@medusajs/framework/http" - -export default defineMiddlewares({ - routes: [ - { - method: ["POST"], - bodyParser: { sizeLimit: "2mb" }, - matcher: "/custom", - }, - ], -}) -``` - -The `sizeLimit` property accepts one of the following types of values: - -- A string representing the size limit in bytes (For example, `100kb`, `2mb`, `5gb`). It is passed to the [bytes](https://www.npmjs.com/package/bytes) library to parse the size. -- A number representing the size limit in bytes. For example, `1024` for 1kb. - -*** - -## Configure File Uploads - -To accept file uploads in your API routes, you can configure the [Express Multer middleware](https://expressjs.com/en/resources/middleware/multer.html) on your route. - -The `multer` package is available through the `@medusajs/medusa` package, so you don't need to install it. However, for better typing support, install the `@types/multer` package as a development dependency: - -```bash npm2yarn -npm install --save-dev @types/multer -``` - -Then, to configure file upload for your route, create the file `src/api/middlewares.ts` with the following content: - -```ts title="src/api/middlewares.ts" highlights={uploadHighlights} -import { defineMiddlewares } from "@medusajs/framework/http" -import multer from "multer" - -const upload = multer({ storage: multer.memoryStorage() }) - -export default defineMiddlewares({ - routes: [ - { - method: ["POST"], - matcher: "/custom", - middlewares: [ - // @ts-ignore - upload.array("files"), - ], - }, - ], -}) -``` - -In the example above, you configure the `multer` middleware to store the uploaded files in memory. Then, you apply the `upload.array("files")` middleware to the route to accept file uploads. By using the `array` method, you accept multiple file uploads with the same `files` field name. - -You can then access the uploaded files in your API route handler: - -```ts title="src/api/custom/route.ts" -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" - -export async function POST( - req: MedusaRequest, - res: MedusaResponse -) { - const files = req.files as Express.Multer.File[] - - // TODO handle files -} -``` - -The uploaded files are stored in the `req.files` property as an array of Multer file objects that have properties like `filename` and `mimetype`. - -### Uploading Files using File Module Provider - -The recommended way to upload the files to storage using the configured [File Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/file/index.html.md) is to use the [uploadFilesWorkflow](https://docs.medusajs.com/resources/references/medusa-workflows/uploadFilesWorkflow/index.html.md): - -```ts title="src/api/custom/route.ts" -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" -import { MedusaError } from "@medusajs/framework/utils" -import { uploadFilesWorkflow } from "@medusajs/medusa/core-flows" - -export async function POST( - req: MedusaRequest, - res: MedusaResponse -) { - const files = req.files as Express.Multer.File[] - - if (!files?.length) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "No files were uploaded" - ) - } - - const { result } = await uploadFilesWorkflow(req.scope).run({ - input: { - files: files?.map((f) => ({ - filename: f.originalname, - mimeType: f.mimetype, - content: f.buffer.toString("binary"), - access: "public", - })), - }, - }) - - res.status(200).json({ files: result }) -} -``` - -Check out the [uploadFilesWorkflow reference](https://docs.medusajs.com/resources/references/medusa-workflows/uploadFilesWorkflow/index.html.md) for details on the expected input and output of the workflow. - - # Protected API Routes In this chapter, you’ll learn how to create protected API routes. @@ -7277,6 +9523,88 @@ This API route opens a stream by setting the `Content-Type` in the header to `te The `MedusaResponse` type is based on [Express's Response](https://expressjs.com/en/api.html#res). Refer to their API reference for other uses of responses. +# Retrieve Custom Links from Medusa's API Route + +In this chapter, you'll learn how to retrieve custom data models linked to existing Medusa data models from Medusa's API routes. + +## Why Retrieve Custom Linked Data Models? + +Often, you'll link custom data models to existing Medusa data models to implement custom features or expand on existing ones. + +For example, to add brands for products, you can create a `Brand` data model in a Brand Module, then [define a link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) to the [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md)'s `Product` data model. + +When you implement this customization, you might need to retrieve the brand of a product using the existing [Get Product API Route](https://docs.medusajs.com/api/admin#products_getproductsid). You can do this by passing the linked data model's name in the `fields` query parameter of the API route. + +*** + +## How to Retrieve Custom Linked Data Models Using `fields`? + +Most of Medusa's API routes accept a `fields` query parameter that allows you to specify the fields and relations to retrieve in the resource, such as a product. + +For example, to retrieve the brand of a product, you can pass the `brand` field in the `fields` query parameter of the [Get Product API Route](https://docs.medusajs.com/api/admin#products_getproductsid): + +```bash +curl 'http://localhost:9000/admin/products/{id}?fields=*brand' \ +-H 'Authorization: Bearer {access_token}' +``` + +The `fields` query parameter accepts a comma-separated list of fields and relations to retrieve. To learn more about using the `fields` query parameter, refer to the [API Reference](https://docs.medusajs.com/api/store#select-fields-and-relations). + +By prefixing `brand` with an asterisk (`*`), you retrieve all the default fields of the product, including the `brand` field. If you don't include the `*` prefix, the response will only include the product's brand. + +*** + +## API Routes that Restrict Retrievable Fields + +Some of Medusa's API routes restrict the fields and relations you can retrieve, which means you can't pass your custom linked data models in the `fields` query parameter. Medusa makes this restriction to ensure the API routes are performant and secure. + +The API routes that restrict the fields and relations you can retrieve are: + +- [Customer Store API Routes](https://docs.medusajs.com/api/store#customers) +- [Customer Admin API Routes](https://docs.medusajs.com/api/admin#customers) +- [Product Category Admin API Routes](https://docs.medusajs.com/api/admin#product-categories) + +### How to Override Allowed Fields and Relations + +For these routes, you need to override the allowed fields and relations to be retrieved. You can do this by adding a [middleware](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md) to those routes. + +For example, to allow retrieving the `b2b_company` of a customer using the [Get Customer Admin API Route](https://docs.medusajs.com/api/admin#customers_getcustomersid), create the file `src/api/middlewares.ts` with the following content: + +Learn how to create a middleware in the [Middlewares](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md) chapter. + +```ts title="src/api/middlewares.ts" highlights={highlights} +import { defineMiddlewares } from "@medusajs/medusa" + +export default defineMiddlewares({ + routes: [ + { + matcher: "/store/customers/me", + method: "GET", + middlewares: [ + (req, res, next) => { + req.allowed?.push("b2b_company") + next() + }, + ], + }, + ], +}) +``` + +In this example, you apply a middleware to the [Get Customer Admin API Route](https://docs.medusajs.com/api/admin#customers_getcustomersid). + +The request object passed to middlewares has an `allowed` property that contains the fields and relations that can be retrieved. So, you modify the `allowed` array to include the `b2b_company` field. + +You can now retrieve the `b2b_company` field using the `fields` query parameter of the [Get Customer Admin API Route](https://docs.medusajs.com/api/admin#customers_getcustomersid): + +```bash +curl 'http://localhost:9000/admin/customers/{id}?fields=*b2b_company' \ +-H 'Authorization: Bearer {access_token}' +``` + +In this example, you retrieve the `b2b_company` relation of the customer using the `fields` query parameter. + + # Request Body and Query Parameter Validation In this chapter, you'll learn how to validate request body and query parameters in your custom API route. @@ -7526,158 +9854,49 @@ 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). -# Retrieve Custom Links from Medusa's API Route +# Event Data Payload -In this chapter, you'll learn how to retrieve custom data models linked to existing Medusa data models from Medusa's API routes. +In this chapter, you'll learn how subscribers receive an event's data payload. -## Why Retrieve Custom Linked Data Models? +## Access Event's Data Payload -Often, you'll link custom data models to existing Medusa data models to implement custom features or expand on existing ones. +When events are emitted, they’re emitted with a data payload. -For example, to add brands for products, you can create a `Brand` data model in a Brand Module, then [define a link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) to the [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md)'s `Product` data model. +The object that the subscriber function receives as a parameter has an `event` property, which is an object holding the event payload in a `data` property with additional context. -When you implement this customization, you might need to retrieve the brand of a product using the existing [Get Product API Route](https://docs.medusajs.com/api/admin#products_getproductsid). You can do this by passing the linked data model's name in the `fields` query parameter of the API route. +For example: -*** +```ts title="src/subscribers/product-created.ts" highlights={highlights} collapsibleLines="1-5" expandButtonLabel="Show Imports" +import type { + SubscriberArgs, + SubscriberConfig, +} from "@medusajs/framework" -## How to Retrieve Custom Linked Data Models Using `fields`? +export default async function productCreateHandler({ + event, +}: SubscriberArgs<{ id: string }>) { + const productId = event.data.id + console.log(`The product ${productId} was created`) +} -Most of Medusa's API routes accept a `fields` query parameter that allows you to specify the fields and relations to retrieve in the resource, such as a product. - -For example, to retrieve the brand of a product, you can pass the `brand` field in the `fields` query parameter of the [Get Product API Route](https://docs.medusajs.com/api/admin#products_getproductsid): - -```bash -curl 'http://localhost:9000/admin/products/{id}?fields=*brand' \ --H 'Authorization: Bearer {access_token}' +export const config: SubscriberConfig = { + event: "product.created", +} ``` -The `fields` query parameter accepts a comma-separated list of fields and relations to retrieve. To learn more about using the `fields` query parameter, refer to the [API Reference](https://docs.medusajs.com/api/store#select-fields-and-relations). +The `event` object has the following properties: -By prefixing `brand` with an asterisk (`*`), you retrieve all the default fields of the product, including the `brand` field. If you don't include the `*` prefix, the response will only include the product's brand. +- data: (\`object\`) The data payload of the event. Its properties are different for each event. +- name: (string) The name of the triggered event. +- metadata: (\`object\`) Additional data and context of the emitted event. -*** +This logs the product ID received in the `product.created` event’s data payload to the console. -## API Routes that Restrict Retrievable Fields +{/* --- -Some of Medusa's API routes restrict the fields and relations you can retrieve, which means you can't pass your custom linked data models in the `fields` query parameter. Medusa makes this restriction to ensure the API routes are performant and secure. +## List of Events with Data Payload -The API routes that restrict the fields and relations you can retrieve are: - -- [Customer Store API Routes](https://docs.medusajs.com/api/store#customers) -- [Customer Admin API Routes](https://docs.medusajs.com/api/admin#customers) -- [Product Category Admin API Routes](https://docs.medusajs.com/api/admin#product-categories) - -### How to Override Allowed Fields and Relations - -For these routes, you need to override the allowed fields and relations to be retrieved. You can do this by adding a [middleware](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md) to those routes. - -For example, to allow retrieving the `b2b_company` of a customer using the [Get Customer Admin API Route](https://docs.medusajs.com/api/admin#customers_getcustomersid), create the file `src/api/middlewares.ts` with the following content: - -Learn how to create a middleware in the [Middlewares](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md) chapter. - -```ts title="src/api/middlewares.ts" highlights={highlights} -import { defineMiddlewares } from "@medusajs/medusa" - -export default defineMiddlewares({ - routes: [ - { - matcher: "/store/customers/me", - method: "GET", - middlewares: [ - (req, res, next) => { - req.allowed?.push("b2b_company") - next() - }, - ], - }, - ], -}) -``` - -In this example, you apply a middleware to the [Get Customer Admin API Route](https://docs.medusajs.com/api/admin#customers_getcustomersid). - -The request object passed to middlewares has an `allowed` property that contains the fields and relations that can be retrieved. So, you modify the `allowed` array to include the `b2b_company` field. - -You can now retrieve the `b2b_company` field using the `fields` query parameter of the [Get Customer Admin API Route](https://docs.medusajs.com/api/admin#customers_getcustomersid): - -```bash -curl 'http://localhost:9000/admin/customers/{id}?fields=*b2b_company' \ --H 'Authorization: Bearer {access_token}' -``` - -In this example, you retrieve the `b2b_company` relation of the customer using the `fields` query parameter. - - -# 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. +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 @@ -7868,45 +10087,245 @@ 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. -# Infer Type of Data Model +# Emit Workflow and Service Events -In this chapter, you'll learn how to infer the type of a data model. +In this chapter, you'll learn about event types and how to emit an event in a service or workflow. -## How to Infer Type of Data Model? +## Event Types -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. +In your customization, you can emit an event, then listen to it in a subscriber and perform an asynchronus action, such as send a notification or data to a third-party system. -Instead, Medusa provides `InferTypeOf` that transforms your data model to a type. +There are two types of events in Medusa: + +1. Workflow event: an event that's emitted in a workflow after a commerce feature is performed. For example, Medusa emits the `order.placed` event after a cart is completed. +2. Service event: an event that's emitted to track, trace, or debug processes under the hood. For example, you can emit an event with an audit trail. + +### Which Event Type to Use? + +**Workflow events** are the most common event type in development, as most custom features and customizations are built around workflows. + +Some examples of workflow events: + +1. When a user creates a blog post and you're emitting an event to send a newsletter email. +2. When you finish syncing products to a third-party system and you want to notify the admin user of new products added. +3. When a customer purchases a digital product and you want to generate and send it to them. + +You should only go for a **service event** if you're emitting an event for processes under the hood that don't directly affect front-facing features. + +Some examples of service events: + +1. When you're tracing data manipulation and changes, and you want to track every time some custom data is changed. +2. When you're syncing data with a search engine. + +*** + +## Emit Event in a Workflow + +To emit a workflow event, use the `emitEventStep` helper step provided in the `@medusajs/medusa/core-flows` package. For example: -```ts -import { InferTypeOf } from "@medusajs/framework/types" -import { Post } from "../modules/blog/models/post" // relative path to the model +```ts highlights={highlights} +import { + createWorkflow, +} from "@medusajs/framework/workflows-sdk" +import { + emitEventStep, +} from "@medusajs/medusa/core-flows" -export type Post = InferTypeOf +const helloWorldWorkflow = createWorkflow( + "hello-world", + () => { + // ... + + emitEventStep({ + eventName: "custom.created", + data: { + id: "123", + // other data payload + }, + }) + } +) ``` -`InferTypeOf` accepts as a type argument the type of the data model. +The `emitEventStep` accepts an object having the following properties: -Since the `Post` data model is a variable, use the `typeof` operator to pass the data model as a type argument to `InferTypeOf`. +- `eventName`: The event's name. +- `data`: The data payload as an object. You can pass any properties in the object, and subscribers listening to the event will receive this data in the event's payload. -You can now use the `Post` type to reference a data model in other types, such as in workflow inputs or service method outputs: +In this example, you emit the event `custom.created` and pass in the data payload an ID property. -```ts title="Example Service" -// other imports... -import { InferTypeOf } from "@medusajs/framework/types" -import { Post } from "../models/post" +### Test it Out -type Post = InferTypeOf +If you execute the workflow, the event is emitted and you can see it in your application's logs. -class BlogModuleService extends MedusaService({ Post }) { - async doSomething(): Promise { - // ... +Any subscribers listening to the event are executed. + +*** + +## Emit Event in a Service + +To emit a service event: + +1. Resolve `event_bus` from the module's container in your service's constructor: + +### Extending Service Factory + +```ts title="src/modules/blog/service.ts" highlights={["9"]} +import { IEventBusService } from "@medusajs/framework/types" +import { MedusaService } from "@medusajs/framework/utils" + +class BlogModuleService extends MedusaService({ + Post, +}){ + protected eventBusService_: AbstractEventBusModuleService + + constructor({ event_bus }) { + super(...arguments) + this.eventBusService_ = event_bus } } ``` +### Without Service Factory + +```ts title="src/modules/blog/service.ts" highlights={["6"]} +import { IEventBusService } from "@medusajs/framework/types" + +class BlogModuleService { + protected eventBusService_: AbstractEventBusModuleService + + constructor({ event_bus }) { + this.eventBusService_ = event_bus + } +} +``` + +2. Use the event bus service's `emit` method in the service's methods to emit an event: + +```ts title="src/modules/blog/service.ts" highlights={serviceHighlights} +class BlogModuleService { + // ... + performAction() { + // TODO perform action + + this.eventBusService_.emit({ + name: "custom.event", + data: { + id: "123", + // other data payload + }, + }) + } +} +``` + +The method accepts an object having the following properties: + +- `name`: The event's name. +- `data`: The data payload as an object. You can pass any properties in the object, and subscribers listening to the event will receive this data in the event's payload. + +3. By default, the Event Module's service isn't injected into your module's container. To add it to the container, pass it in the module's registration object in `medusa-config.ts` in the `dependencies` property: + +```ts title="medusa-config.ts" highlights={depsHighlight} +import { Modules } from "@medusajs/framework/utils" + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "./src/modules/blog", + dependencies: [ + Modules.EVENT_BUS, + ], + }, + ], +}) +``` + +The `dependencies` property accepts an array of module registration keys. The specified modules' main services are injected into the module's container. + +That's how you can resolve it in your module's main service's constructor. + +### Test it Out + +If you execute the `performAction` method of your service, the event is emitted and you can see it in your application's logs. + +Any subscribers listening to the event are also executed. + + +# Add Data Model Check Constraints + +In this chapter, you'll learn how to add check constraints to your data model. + +## 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 @@ -8020,6 +10439,46 @@ export default MyCustom This creates a unique composite index on the `name` and `age` properties. +# Infer Type of Data Model + +In this chapter, you'll learn how to infer the type of a data model. + +## How to Infer Type of Data Model? + +Consider you have a `Post` data model. You can't reference this data model in a type, such as a workflow input or service method output types, since it's a variable. + +Instead, Medusa provides `InferTypeOf` that transforms your data model to a type. + +For example: + +```ts +import { InferTypeOf } from "@medusajs/framework/types" +import { Post } from "../modules/blog/models/post" // relative path to the model + +export type Post = InferTypeOf +``` + +`InferTypeOf` accepts as a type argument the type of the data model. + +Since the `Post` data model is a variable, use the `typeof` operator to pass the data model as a type argument to `InferTypeOf`. + +You can now use the `Post` type to reference a data model in other types, such as in workflow inputs or service method outputs: + +```ts title="Example Service" +// other imports... +import { InferTypeOf } from "@medusajs/framework/types" +import { Post } from "../models/post" + +type Post = InferTypeOf + +class BlogModuleService extends MedusaService({ Post }) { + async doSomething(): Promise { + // ... + } +} +``` + + # Manage Relationships In this chapter, you'll learn how to manage relationships between data models when creating, updating, or retrieving records using the module's main service. @@ -8240,358 +10699,6 @@ const product = await blogModuleService.retrieveProducts( In the example above, the retrieved product has an `orders` property, whose value is an array of orders associated with the product. -# 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. @@ -8985,6 +11092,358 @@ So, always rollback the migration before deleting it. 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. @@ -9143,280 +11602,6 @@ await link.create({ ``` -# Event Data Payload - -In this chapter, you'll learn how subscribers receive an event's data payload. - -## Access Event's Data Payload - -When events are emitted, they’re emitted with a data payload. - -The object that the subscriber function receives as a parameter has an `event` property, which is an object holding the event payload in a `data` property with additional context. - -For example: - -```ts title="src/subscribers/product-created.ts" highlights={highlights} collapsibleLines="1-5" expandButtonLabel="Show Imports" -import type { - SubscriberArgs, - SubscriberConfig, -} from "@medusajs/framework" - -export default async function productCreateHandler({ - event, -}: SubscriberArgs<{ id: string }>) { - const productId = event.data.id - console.log(`The product ${productId} was created`) -} - -export const config: SubscriberConfig = { - event: "product.created", -} -``` - -The `event` object has the following properties: - -- data: (\`object\`) The data payload of the event. Its properties are different for each event. -- name: (string) The name of the triggered event. -- metadata: (\`object\`) Additional data and context of the emitted event. - -This logs the product ID received in the `product.created` event’s data payload to the console. - -{/* --- - -## List of Events with Data Payload - -Refer to [this reference](!resources!/references/events) for a full list of events emitted by Medusa and their data payloads. */} - - -# Emit Workflow and Service Events - -In this chapter, you'll learn about event types and how to emit an event in a service or workflow. - -## Event Types - -In your customization, you can emit an event, then listen to it in a subscriber and perform an asynchronus action, such as send a notification or data to a third-party system. - -There are two types of events in Medusa: - -1. Workflow event: an event that's emitted in a workflow after a commerce feature is performed. For example, Medusa emits the `order.placed` event after a cart is completed. -2. Service event: an event that's emitted to track, trace, or debug processes under the hood. For example, you can emit an event with an audit trail. - -### Which Event Type to Use? - -**Workflow events** are the most common event type in development, as most custom features and customizations are built around workflows. - -Some examples of workflow events: - -1. When a user creates a blog post and you're emitting an event to send a newsletter email. -2. When you finish syncing products to a third-party system and you want to notify the admin user of new products added. -3. When a customer purchases a digital product and you want to generate and send it to them. - -You should only go for a **service event** if you're emitting an event for processes under the hood that don't directly affect front-facing features. - -Some examples of service events: - -1. When you're tracing data manipulation and changes, and you want to track every time some custom data is changed. -2. When you're syncing data with a search engine. - -*** - -## Emit Event in a Workflow - -To emit a workflow event, use the `emitEventStep` helper step provided in the `@medusajs/medusa/core-flows` package. - -For example: - -```ts highlights={highlights} -import { - createWorkflow, -} from "@medusajs/framework/workflows-sdk" -import { - emitEventStep, -} from "@medusajs/medusa/core-flows" - -const helloWorldWorkflow = createWorkflow( - "hello-world", - () => { - // ... - - emitEventStep({ - eventName: "custom.created", - data: { - id: "123", - // other data payload - }, - }) - } -) -``` - -The `emitEventStep` accepts an object having the following properties: - -- `eventName`: The event's name. -- `data`: The data payload as an object. You can pass any properties in the object, and subscribers listening to the event will receive this data in the event's payload. - -In this example, you emit the event `custom.created` and pass in the data payload an ID property. - -### Test it Out - -If you execute the workflow, the event is emitted and you can see it in your application's logs. - -Any subscribers listening to the event are executed. - -*** - -## Emit Event in a Service - -To emit a service event: - -1. Resolve `event_bus` from the module's container in your service's constructor: - -### Extending Service Factory - -```ts title="src/modules/blog/service.ts" highlights={["9"]} -import { IEventBusService } from "@medusajs/framework/types" -import { MedusaService } from "@medusajs/framework/utils" - -class BlogModuleService extends MedusaService({ - Post, -}){ - protected eventBusService_: AbstractEventBusModuleService - - constructor({ event_bus }) { - super(...arguments) - this.eventBusService_ = event_bus - } -} -``` - -### Without Service Factory - -```ts title="src/modules/blog/service.ts" highlights={["6"]} -import { IEventBusService } from "@medusajs/framework/types" - -class BlogModuleService { - protected eventBusService_: AbstractEventBusModuleService - - constructor({ event_bus }) { - this.eventBusService_ = event_bus - } -} -``` - -2. Use the event bus service's `emit` method in the service's methods to emit an event: - -```ts title="src/modules/blog/service.ts" highlights={serviceHighlights} -class BlogModuleService { - // ... - performAction() { - // TODO perform action - - this.eventBusService_.emit({ - name: "custom.event", - data: { - id: "123", - // other data payload - }, - }) - } -} -``` - -The method accepts an object having the following properties: - -- `name`: The event's name. -- `data`: The data payload as an object. You can pass any properties in the object, and subscribers listening to the event will receive this data in the event's payload. - -3. By default, the Event Module's service isn't injected into your module's container. To add it to the container, pass it in the module's registration object in `medusa-config.ts` in the `dependencies` property: - -```ts title="medusa-config.ts" highlights={depsHighlight} -import { Modules } from "@medusajs/framework/utils" - -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "./src/modules/blog", - dependencies: [ - Modules.EVENT_BUS, - ], - }, - ], -}) -``` - -The `dependencies` property accepts an array of module registration keys. The specified modules' main services are injected into the module's container. - -That's how you can resolve it in your module's main service's constructor. - -### Test it Out - -If you execute the `performAction` method of your service, the event is emitted and you can see it in your application's logs. - -Any subscribers listening to the event are also executed. - - -# 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 -) -``` - - # Link In this chapter, you’ll learn what Link is and how to use it to manage links. @@ -10403,6 +12588,67 @@ In this example, you retrieve products and their associated posts. You also pass 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. @@ -10909,6 +13155,470 @@ 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 + +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. + + +# Create a Plugin + +In this chapter, you'll learn how to create a Medusa plugin and publish it. + +A [plugin](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md) is a package of reusable Medusa customizations that you can install in any Medusa application. By creating and publishing a plugin, you can reuse your Medusa customizations across multiple projects or share them with the community. + +Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). + +## 1. Create a Plugin Project + +Plugins are created in a separate Medusa project. This makes the development and publishing of the plugin easier. Later, you'll install that plugin in your Medusa application to test it out and use it. + +Medusa's `create-medusa-app` CLI tool provides the option to create a plugin project. Run the following command to create a new plugin project: + +```bash +npx create-medusa-app my-plugin --plugin +``` + +This will create a new Medusa plugin project in the `my-plugin` directory. + +### Plugin Directory Structure + +After the installation is done, the plugin structure will look like this: + +![Directory structure of a plugin project](https://res.cloudinary.com/dza7lstvk/image/upload/v1737019441/Medusa%20Book/project-dir_q4xtri.jpg) + +- `src/`: Contains the Medusa customizations. +- `src/admin`: Contains [admin extensions](https://docs.medusajs.com/learn/fundamentals/admin/index.html.md). +- `src/api`: Contains [API routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) and [middlewares](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md). You can add store, admin, or any custom API routes. +- `src/jobs`: Contains [scheduled jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md). +- `src/links`: Contains [module links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md). +- `src/modules`: Contains [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). +- `src/provider`: Contains [module providers](#create-module-providers). +- `src/subscribers`: Contains [subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). +- `src/workflows`: Contains [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). You can also add [hooks](https://docs.medusajs.com/learn/fundamentals/workflows/add-workflow-hook/index.html.md) under `src/workflows/hooks`. +- `package.json`: Contains the plugin's package information, including general information and dependencies. +- `tsconfig.json`: Contains the TypeScript configuration for the plugin. + +*** + +## 2. Prepare Plugin + +### Package Name + +Before developing, testing, and publishing your plugin, make sure its name in `package.json` is correct. This is the name you'll use to install the plugin in your Medusa application. + +For example: + +```json title="package.json" +{ + "name": "@myorg/plugin-name", + // ... +} +``` + +### Package Keywords + +Medusa scrapes NPM for a list of plugins that integrate third-party services, to later showcase them on the Medusa website. If you want your plugin to appear in that listing, make sure to add the `medusa-v2` and `medusa-plugin-integration` keywords to the `keywords` field in `package.json`. + +Only plugins that integrate third-party services are listed in the Medusa integrations page. If your plugin doesn't integrate a third-party service, you can skip this step. + +```json title="package.json" +{ + "keywords": [ + "medusa-plugin-integration", + "medusa-v2" + ], + // ... +} +``` + +In addition, make sure to use one of the following keywords based on your integration type: + +|Keyword|Description|Example| +|---|---|---| +|\`medusa-plugin-analytics\`|Analytics service integration|Google Analytics| +|\`medusa-plugin-auth\`|Authentication service integration|Auth0| +|\`medusa-plugin-cms\`|CMS service integration|Contentful| +|\`medusa-plugin-notification\`|Notification service integration|Twilio SMS| +|\`medusa-plugin-payment\`|Payment service integration|PayPal| +|\`medusa-plugin-search\`|Search service integration|MeiliSearch| +|\`medusa-plugin-shipping\`|Shipping service integration|DHL| +|\`medusa-plugin-other\`|Other third-party integrations|Sentry| + +### Package Dependencies + +Your plugin project will already have the dependencies mentioned in this section. Unless you made changes to the dependencies, you can skip this section. + +In the `package.json` file you must have the Medusa dependencies as `devDependencies` and `peerDependencies`. In addition, you must have `@swc/core` as a `devDependency`, as it's used by the plugin CLI tools. + +For example, assuming `2.5.0` is the latest Medusa version: + +```json title="package.json" +{ + "devDependencies": { + "@medusajs/admin-sdk": "2.5.0", + "@medusajs/cli": "2.5.0", + "@medusajs/framework": "2.5.0", + "@medusajs/medusa": "2.5.0", + "@medusajs/test-utils": "2.5.0", + "@medusajs/ui": "4.0.4", + "@medusajs/icons": "2.5.0", + "@swc/core": "1.5.7", + }, + "peerDependencies": { + "@medusajs/admin-sdk": "2.5.0", + "@medusajs/cli": "2.5.0", + "@medusajs/framework": "2.5.0", + "@medusajs/test-utils": "2.5.0", + "@medusajs/medusa": "2.5.0", + "@medusajs/ui": "4.0.3", + "@medusajs/icons": "2.5.0", + } +} +``` + +### Package Exports + +Your plugin project will already have the exports mentioned in this section. Unless you made changes to the exports or you created your plugin before [Medusa v2.7.0](https://github.com/medusajs/medusa/releases/tag/v2.7.0), you can skip this section. + +In the `package.json` file, make sure your plugin has the following exports: + +```json title="package.json" +{ + "exports": { + "./package.json": "./package.json", + "./workflows": "./.medusa/server/src/workflows/index.js", + "./.medusa/server/src/modules/*": "./.medusa/server/src/modules/*/index.js", + "./providers/*": "./.medusa/server/src/providers/*/index.js", + "./admin": { + "import": "./.medusa/server/src/admin/index.mjs", + "require": "./.medusa/server/src/admin/index.js", + "default": "./.medusa/server/src/admin/index.js" + }, + "./*": "./.medusa/server/src/*.js" + } +} +``` + +Aside from the `./package.json`, `./providers`, and `./admin`, these exports are only a recommendation. You can cherry-pick the files and directories you want to export. + +The plugin exports the following files and directories: + +- `./package.json`: The `package.json` file. Medusa needs to access the `package.json` when registering the plugin. +- `./workflows`: The workflows exported in `./src/workflows/index.ts`. +- `./.medusa/server/src/modules/*`: The definition file of modules. This is useful if you create links to the plugin's modules in the Medusa application. +- `./providers/*`: The definition file of module providers. This is useful if your plugin includes a module provider, allowing you to register the plugin's providers in Medusa applications. Learn more in the [Create Module Providers](#create-module-providers) section. +- `./admin`: The admin extensions exported in `./src/admin/index.ts`. +- `./*`: Any other files in the plugin's `src` directory. + +*** + +## 3. Publish Plugin Locally for Development and Testing + +Medusa's CLI tool provides commands to simplify developing and testing your plugin in a local Medusa application. You start by publishing your plugin in the local package registry, then install it in your Medusa application. You can then watch for changes in the plugin as you develop it. + +### Publish and Install Local Package + +### Prerequisites + +- [Medusa application installed.](https://docs.medusajs.com/learn/installation/index.html.md) + +The first time you create your plugin, you need to publish the package into a local package registry, then install it in your Medusa application. This is a one-time only process. + +To publish the plugin to the local registry, run the following command in your plugin project: + +```bash title="Plugin project" +npx medusa plugin:publish +``` + +This command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. The plugin is published locally under the name you specified in `package.json`. + +Next, navigate to your Medusa application: + +```bash title="Medusa application" +cd ~/path/to/medusa-app +``` + +Make sure to replace `~/path/to/medusa-app` with the path to your Medusa application. + +Then, if your project was created before v2.3.1 of Medusa, make sure to install `yalc` as a development dependency: + +```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green" +npm install --save-dev yalc +``` + +After that, run the following Medusa CLI command to install the plugin: + +```bash title="Medusa application" +npx medusa plugin:add @myorg/plugin-name +``` + +Make sure to replace `@myorg/plugin-name` with the name of your plugin as specified in `package.json`. Your plugin will be installed from the local package registry into your Medusa application. + +### Register Plugin in Medusa Application + +After installing the plugin, you need to register it in your Medusa application in the configurations defined in `medusa-config.ts`. + +Add the plugin to the `plugins` array in the `medusa-config.ts` file: + +```ts title="medusa-config.ts" highlights={pluginHighlights} +module.exports = defineConfig({ + // ... + plugins: [ + { + resolve: "@myorg/plugin-name", + options: {}, + }, + ], +}) +``` + +The `plugins` configuration is an array of objects where each object has a `resolve` key whose value is the name of the plugin package. + +#### Pass Module Options through Plugin + +Each plugin configuration also accepts an `options` property, whose value is an object of options to pass to the plugin's modules. + +For example: + +```ts title="medusa-config.ts" highlights={pluginOptionsHighlight} +module.exports = defineConfig({ + // ... + plugins: [ + { + resolve: "@myorg/plugin-name", + options: { + apiKey: true, + }, + }, + ], +}) +``` + +The `options` property in the plugin configuration is passed to all modules in the plugin. Learn more about module options in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/options/index.html.md). + +### Watch Plugin Changes During Development + +While developing your plugin, you can watch for changes in the plugin and automatically update the plugin in the Medusa application using it. This is the only command you'll continuously need during your plugin development. + +To do that, run the following command in your plugin project: + +```bash title="Plugin project" +npx medusa plugin:develop +``` + +This command will: + +- Watch for changes in the plugin. Whenever a file is changed, the plugin is automatically built. +- Publish the plugin changes to the local package registry. This will automatically update the plugin in the Medusa application using it. You can also benefit from real-time HMR updates of admin extensions. + +### Start Medusa Application + +You can start your Medusa application's development server to test out your plugin: + +```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green" +npm run dev +``` + +While your Medusa application is running and the plugin is being watched, you can test your plugin while developing it in the Medusa application. + +*** + +## 4. Create Customizations in the Plugin + +You can now build your plugin's customizations. The following guide explains how to build different customizations in your plugin. + +- [Create a module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) +- [Create a module link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) +- [Create a workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) +- [Add a workflow hook](https://docs.medusajs.com/learn/fundamentals/workflows/add-workflow-hook/index.html.md) +- [Create an API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) +- [Add a subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) +- [Add a scheduled job](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) +- [Add an admin widget](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md) +- [Add an admin UI route](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md) + +While building those customizations, you can test them in your Medusa application by [watching the plugin changes](#watch-plugin-changes-during-development) and [starting the Medusa application](#start-medusa-application). + +### Generating Migrations for Modules + +During your development, you may need to generate migrations for modules in your plugin. To do that, first, add the following environment variables in your plugin project: + +```plain title="Plugin project" +DB_USERNAME=postgres +DB_PASSWORD=123... +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=db_name +``` + +You can add these environment variables in a `.env` file in your plugin project. The variables are: + +- `DB_USERNAME`: The username of the PostgreSQL user to connect to the database. +- `DB_PASSWORD`: The password of the PostgreSQL user to connect to the database. +- `DB_HOST`: The host of the PostgreSQL database. Typically, it's `localhost` if you're running the database locally. +- `DB_PORT`: The port of the PostgreSQL database. Typically, it's `5432` if you're running the database locally. +- `DB_NAME`: The name of the PostgreSQL database to connect to. + +Then, run the following command in your plugin project to generate migrations for the modules in your plugin: + +```bash title="Plugin project" +npx medusa plugin:db:generate +``` + +This command generates migrations for all modules in the plugin. + +Finally, run these migrations on the Medusa application that the plugin is installed in using the `db:migrate` command: + +```bash title="Medusa application" +npx medusa db:migrate +``` + +The migrations in your application, including your plugin, will run and update the database. + +### Importing Module Resources + +In the [Prepare Plugin](#2-prepare-plugin) section, you learned about [exported resources](#package-exports) in your plugin. + +These exports allow you to import your plugin resources in your Medusa application, including workflows, links and modules. + +For example, to import the plugin's workflow in your Medusa application: + +`@myorg/plugin-name` is the plugin package's name. + +```ts +import { Workflow1, Workflow2 } from "@myorg/plugin-name/workflows" +import BlogModule from "@myorg/plugin-name/modules/blog" +// import other files created in plugin like ./src/types/blog.ts +import BlogType from "@myorg/plugin-name/types/blog" +``` + +### Create Module Providers + +The [exported resources](#package-exports) also allow you to import module providers in your plugin and register them in the Medusa application's configuration. A module provider is a module that provides the underlying logic or integration related to a commerce or Infrastructure Module. + +For example, assuming your plugin has a [Notification Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/notification/index.html.md) called `my-notification`, you can register it in your Medusa application's configuration like this: + +`@myorg/plugin-name` is the plugin package's name. + +```ts highlights={[["9"]]} title="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/notification", + options: { + providers: [ + { + resolve: "@myorg/plugin-name/providers/my-notification", + id: "my-notification", + options: { + channels: ["email"], + // provider options... + }, + }, + ], + }, + }, + ], +}) +``` + +You pass to `resolve` the path to the provider relative to the plugin package. So, in this example, the `my-notification` provider is located in `./src/providers/my-notification/index.ts` of the plugin. + +To learn how to create module providers, refer to the following guides: + +- [File Module Provider](https://docs.medusajs.com/resources/references/file-provider-module/index.html.md) +- [Notification Module Provider](https://docs.medusajs.com/resources/references/notification-provider-module/index.html.md) +- [Auth Module Provider](https://docs.medusajs.com/resources/references/auth/provider/index.html.md) +- [Payment Module Provider](https://docs.medusajs.com/resources/references/payment/provider/index.html.md) +- [Fulfillment Module Provider](https://docs.medusajs.com/resources/references/fulfillment/provider/index.html.md) +- [Tax Module Provider](https://docs.medusajs.com/resources/references/tax/provider/index.html.md) + +*** + +## 5. Publish Plugin to NPM + +Make sure to add the keywords mentioned in the [Package Keywords](#package-keywords) section in your plugin's `package.json` file. + +Medusa's CLI tool provides a command that bundles your plugin to be published to npm. Once you're ready to publish your plugin publicly, run the following command in your plugin project: + +```bash +npx medusa plugin:build +``` + +The command will compile an output in the `.medusa/server` directory. + +You can now publish the plugin to npm using the [NPM CLI tool](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). Run the following command to publish the plugin to npm: + +```bash +npm publish +``` + +If you haven't logged in before with your NPM account, you'll be asked to log in first. Then, your package is published publicly to be used in any Medusa application. + +### Install Public Plugin in Medusa Application + +You install a plugin that's published publicly using your package manager. For example: + +```bash npm2yarn +npm install @myorg/plugin-name +``` + +Where `@myorg/plugin-name` is the name of your plugin as published on NPM. + +Then, register the plugin in your Medusa application's configurations as explained in [this section](#register-plugin-in-medusa-application). + +*** + +## Update a Published Plugin + +To update the Medusa dependencies in a plugin, refer to [this documentation](https://docs.medusajs.com/learn/update#update-plugin-project/index.html.md). + +If you've published a plugin and you've made changes to it, you'll have to publish the update to NPM again. + +First, run the following command to change the version of the plugin: + +```bash +npm version +``` + +Where `` indicates the type of version update you’re publishing. For example, it can be `major` or `minor`. Refer to the [npm version documentation](https://docs.npmjs.com/cli/v10/commands/npm-version) for more information. + +Then, re-run the same commands for publishing a plugin: + +```bash +npx medusa plugin:build +npm publish +``` + +This will publish an updated version of your plugin under a new version. + + # Commerce Modules In this chapter, you'll learn about Medusa's Commerce Modules. @@ -11659,2085 +14369,6 @@ There are different Infrastructure Module types including: Refer to the [Infrastructure Modules reference](https://docs.medusajs.com/resources/infrastructure-modules/index.html.md) for a list of Medusa’s Infrastructure Modules, available modules to install, and how to create an Infrastructure Module. -# Modules Directory Structure - -In this document, you'll learn about the expected files and directories in your module. - -![Module Directory Structure Example](https://res.cloudinary.com/dza7lstvk/image/upload/v1714379976/Medusa%20Book/modules-dir-overview_nqq7ne.jpg) - -## index.ts - -The `index.ts` file in the root of your module's directory is the only required file. It must export the module's definition as explained in a [previous chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). - -*** - -## service.ts - -A module must have a main service. It's created in the `service.ts` file at the root of your module directory as explained in a [previous chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). - -*** - -## Other Directories - -The following directories are optional and their content are explained more in the following chapters: - -- `models`: Holds the data models representing tables in the database. -- `migrations`: Holds the migration files used to reflect changes on the database. -- `loaders`: Holds the scripts to run on the Medusa application's start-up. - - -# Multiple Services in a Module - -In this chapter, you'll learn how to use multiple services in a module. - -## Module's Main and Internal Services - -A module has one main service only, which is the service exported in the module's definition. - -However, you may use other services in your module to better organize your code or split functionalities. These are called internal services that can be resolved within your module, but not in external resources. - -*** - -## How to Add an Internal Service - -### 1. Create Service - -To add an internal service, create it in the `services` directory of your module. - -For example, create the file `src/modules/blog/services/client.ts` with the following content: - -```ts title="src/modules/blog/services/client.ts" -export class ClientService { - async getMessage(): Promise { - return "Hello, World!" - } -} -``` - -### 2. Export Service in Index - -Next, create an `index.ts` file under the `services` directory of the module that exports your internal services. - -For example, create the file `src/modules/blog/services/index.ts` with the following content: - -```ts title="src/modules/blog/services/index.ts" -export * from "./client" -``` - -This exports the `ClientService`. - -### 3. Resolve Internal Service - -Internal services exported in the `services/index.ts` file of your module are now registered in the container and can be resolved in other services in the module as well as loaders. - -For example, in your main service: - -```ts title="src/modules/blog/service.ts" highlights={[["5"], ["13"]]} -// other imports... -import { ClientService } from "./services" - -type InjectedDependencies = { - clientService: ClientService -} - -class BlogModuleService extends MedusaService({ - Post, -}){ - protected clientService_: ClientService - - constructor({ clientService }: InjectedDependencies) { - super(...arguments) - this.clientService_ = clientService - } -} -``` - -You can now use your internal service in your main service. - -*** - -## Resolve Resources in Internal Service - -Resolve dependencies from your module's container in the constructor of your internal service. - -For example: - -```ts -import { Logger } from "@medusajs/framework/types" - -type InjectedDependencies = { - logger: Logger -} - -export class ClientService { - protected logger_: Logger - - constructor({ logger }: InjectedDependencies) { - this.logger_ = logger - } -} -``` - -*** - -## Access Module Options - -Your internal service can't access the module's options. - -To retrieve the module's options, use the `configModule` registered in the module's container, which is the configurations in `medusa-config.ts`. - -For example: - -```ts -import { ConfigModule } from "@medusajs/framework/types" -import { BLOG_MODULE } from ".." - -export type InjectedDependencies = { - configModule: ConfigModule -} - -export class ClientService { - protected options: Record - - constructor({ configModule }: InjectedDependencies) { - const moduleDef = configModule.modules[BLOG_MODULE] - - if (typeof moduleDef !== "boolean") { - this.options = moduleDef.options - } - } -} -``` - -The `configModule` has a `modules` property that includes all registered modules. Retrieve the module's configuration using its registration key. - -If its value is not a `boolean`, set the service's options to the module configuration's `options` property. - - -# Module Isolation - -In this chapter, you'll learn how modules are isolated, and what that means for your custom development. - -- Modules can't access resources, such as services or data models, from other modules. -- Use [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 - }, - }) - } -} -``` - - -# Module Options - -In this chapter, you’ll learn about passing options to your module from the Medusa application’s configurations and using them in the module’s resources. - -## What are Module Options? - -A module can receive options to customize or configure its functionality. For example, if you’re creating a module that integrates a third-party service, you’ll want to receive the integration credentials in the options rather than adding them directly in your code. - -*** - -## How to Pass Options to a Module? - -To pass options to a module, add an `options` property to the module’s configuration in `medusa-config.ts`. - -For example: - -```ts title="medusa-config.ts" -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "./src/modules/blog", - options: { - capitalize: true, - }, - }, - ], -}) -``` - -The `options` property’s value is an object. You can pass any properties you want. - -### Pass Options to a Module in a Plugin - -If your module is part of a plugin, you can pass options to the module in the plugin’s configuration. - -For example: - -```ts title="medusa-config.ts" -import { defineConfig } from "@medusajs/framework/utils" -module.exports = defineConfig({ - plugins: [ - { - resolve: "@myorg/plugin-name", - options: { - capitalize: true, - }, - }, - ], -}) -``` - -The `options` property in the plugin configuration is passed to all modules in a plugin. - -*** - -## Access Module Options in Main Service - -The module’s main service receives the module options as a second parameter. - -For example: - -```ts title="src/modules/blog/service.ts" highlights={[["12"], ["14", "options?: ModuleOptions"], ["17"], ["18"], ["19"]]} -import { MedusaService } from "@medusajs/framework/utils" -import Post from "./models/post" - -// recommended to define type in another file -type ModuleOptions = { - capitalize?: boolean -} - -export default class BlogModuleService extends MedusaService({ - Post, -}){ - protected options_: ModuleOptions - - constructor({}, options?: ModuleOptions) { - super(...arguments) - - this.options_ = options || { - capitalize: false, - } - } - - // ... -} -``` - -*** - -## Access Module Options in Loader - -The object that a module’s loaders receive as a parameter has an `options` property holding the module's options. - -For example: - -```ts title="src/modules/blog/loaders/hello-world.ts" highlights={[["11"], ["12", "ModuleOptions", "The type of expected module options."], ["16"]]} -import { - LoaderOptions, -} from "@medusajs/framework/types" - -// recommended to define type in another file -type ModuleOptions = { - capitalize?: boolean -} - -export default async function helloWorldLoader({ - options, -}: LoaderOptions) { - - console.log( - "[BLOG MODULE] Just started the Medusa application!", - options - ) -} -``` - -*** - -## Validate Module Options - -If you expect a certain option and want to throw an error if it's not provided or isn't valid, it's recommended to perform the validation in a loader. The module's service is only instantiated when it's used, whereas the loader runs the when the Medusa application starts. - -So, by performing the validation in the loader, you ensure you can throw an error at an early point, rather than when the module is used. - -For example, to validate that the Hello Module received an `apiKey` option, create the loader `src/modules/loaders/validate.ts`: - -```ts title="src/modules/blog/loaders/validate.ts" -import { LoaderOptions } from "@medusajs/framework/types" -import { MedusaError } from "@medusajs/framework/utils" - -// recommended to define type in another file -type ModuleOptions = { - apiKey?: string -} - -export default async function validationLoader({ - options, -}: LoaderOptions) { - if (!options.apiKey) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Hello Module requires an apiKey option." - ) - } -} -``` - -Then, export the loader in the module's definition file, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/loaders/index.html.md): - -```ts title="src/modules/blog/index.ts" -// other imports... -import validationLoader from "./loaders/validate" - -export const BLOG_MODULE = "blog" - -export default Module(BLOG_MODULE, { - // ... - loaders: [validationLoader], -}) -``` - -Now, when the Medusa application starts, the loader will run, validating the module's options and throwing an error if the `apiKey` option is missing. - - -# Service Constraints - -This chapter lists constraints to keep in mind when creating a service. - -## Use Async Methods - -Medusa wraps service method executions to inject useful context or transactions. However, since Medusa can't detect whether the method is asynchronous, it always executes methods in the wrapper with the `await` keyword. - -For example, if you have a synchronous `getMessage` method, and you use it in other resources like workflows, Medusa executes it as an async method: - -```ts -await blogModuleService.getMessage() -``` - -So, make sure your service's methods are always async to avoid unexpected errors or behavior. - -```ts highlights={[["8", "", "Method must be async."], ["13", "async", "Correct way of defining the method."]]} -import { MedusaService } from "@medusajs/framework/utils" -import Post from "./models/post" - -class BlogModuleService extends MedusaService({ - Post, -}){ - // Don't - getMessage(): string { - return "Hello, World!" - } - - // Do - async getMessage(): Promise { - return "Hello, World!" - } -} - -export default BlogModuleService -``` - - -# Service Factory - -In this chapter, you’ll learn about what the service factory is and how to use it. - -## What is the Service Factory? - -Medusa provides a service factory that your module’s main service can extend. - -The service factory generates data management methods for your data models in the database, so you don't have to implement these methods manually. - -Your service provides data-management functionalities of your data models. - -*** - -## How to Extend the Service Factory? - -Medusa provides the service factory as a `MedusaService` function your service extends. The function creates and returns a service class with generated data-management methods. - -For example, create the file `src/modules/blog/service.ts` with the following content: - -```ts title="src/modules/blog/service.ts" highlights={highlights} -import { MedusaService } from "@medusajs/framework/utils" -import Post from "./models/post" - -class BlogModuleService extends MedusaService({ - Post, -}){ - // TODO implement custom methods -} - -export default BlogModuleService -``` - -### MedusaService Parameters - -The `MedusaService` function accepts one parameter, which is an object of data models to generate data-management methods for. - -In the example above, since the `BlogModuleService` extends `MedusaService`, it has methods to manage the `Post` data model, such as `createPosts`. - -### Generated Methods - -The service factory generates methods to manage the records of each of the data models provided in the first parameter in the database. - -The method's names are the operation's name, suffixed by the data model's key in the object parameter passed to `MedusaService`. - -For example, the following methods are generated for the service above: - -Find a complete reference of each of the methods in [this documentation](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) - -### listPosts - -### listPosts - -This method retrieves an array of records based on filters and pagination configurations. - -For example: - -```ts -const posts = await blogModuleService - .listPosts() - -// with filters -const posts = await blogModuleService - .listPosts({ - id: ["123"] - }) -``` - -### listAndCountPosts - -### retrievePost - -This method retrieves a record by its ID. - -For example: - -```ts -const post = await blogModuleService - .retrievePost("123") -``` - -### retrievePost - -### updatePosts - -This method updates and retrieves records of the data model. - -For example: - -```ts -const post = await blogModuleService - .updatePosts({ - id: "123", - title: "test" - }) - -// update multiple -const posts = await blogModuleService - .updatePosts([ - { - id: "123", - title: "test" - }, - { - id: "321", - title: "test 2" - }, - ]) - -// use filters -const posts = await blogModuleService - .updatePosts([ - { - selector: { - id: ["123", "321"] - }, - data: { - title: "test" - } - }, - ]) -``` - -### createPosts - -### softDeletePosts - -This method soft-deletes records using an array of IDs or an object of filters. - -For example: - -```ts -await blogModuleService.softDeletePosts("123") - -// soft-delete multiple -await blogModuleService.softDeletePosts([ - "123", "321" -]) - -// use filters -await blogModuleService.softDeletePosts({ - id: ["123", "321"] -}) -``` - -### updatePosts - -### deletePosts - -### softDeletePosts - -### restorePosts - -### Using a Constructor - -If you implement the `constructor` of your service, make sure to call `super` passing it `...arguments`. - -For example: - -```ts highlights={[["8"]]} -import { MedusaService } from "@medusajs/framework/utils" -import Post from "./models/post" - -class BlogModuleService extends MedusaService({ - Post, -}){ - constructor() { - super(...arguments) - } -} - -export default BlogModuleService -``` - - -# Create a Plugin - -In this chapter, you'll learn how to create a Medusa plugin and publish it. - -A [plugin](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md) is a package of reusable Medusa customizations that you can install in any Medusa application. By creating and publishing a plugin, you can reuse your Medusa customizations across multiple projects or share them with the community. - -Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). - -## 1. Create a Plugin Project - -Plugins are created in a separate Medusa project. This makes the development and publishing of the plugin easier. Later, you'll install that plugin in your Medusa application to test it out and use it. - -Medusa's `create-medusa-app` CLI tool provides the option to create a plugin project. Run the following command to create a new plugin project: - -```bash -npx create-medusa-app my-plugin --plugin -``` - -This will create a new Medusa plugin project in the `my-plugin` directory. - -### Plugin Directory Structure - -After the installation is done, the plugin structure will look like this: - -![Directory structure of a plugin project](https://res.cloudinary.com/dza7lstvk/image/upload/v1737019441/Medusa%20Book/project-dir_q4xtri.jpg) - -- `src/`: Contains the Medusa customizations. -- `src/admin`: Contains [admin extensions](https://docs.medusajs.com/learn/fundamentals/admin/index.html.md). -- `src/api`: Contains [API routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) and [middlewares](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md). You can add store, admin, or any custom API routes. -- `src/jobs`: Contains [scheduled jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md). -- `src/links`: Contains [module links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md). -- `src/modules`: Contains [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). -- `src/provider`: Contains [module providers](#create-module-providers). -- `src/subscribers`: Contains [subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). -- `src/workflows`: Contains [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). You can also add [hooks](https://docs.medusajs.com/learn/fundamentals/workflows/add-workflow-hook/index.html.md) under `src/workflows/hooks`. -- `package.json`: Contains the plugin's package information, including general information and dependencies. -- `tsconfig.json`: Contains the TypeScript configuration for the plugin. - -*** - -## 2. Prepare Plugin - -### Package Name - -Before developing, testing, and publishing your plugin, make sure its name in `package.json` is correct. This is the name you'll use to install the plugin in your Medusa application. - -For example: - -```json title="package.json" -{ - "name": "@myorg/plugin-name", - // ... -} -``` - -### Package Keywords - -Medusa scrapes NPM for a list of plugins that integrate third-party services, to later showcase them on the Medusa website. If you want your plugin to appear in that listing, make sure to add the `medusa-v2` and `medusa-plugin-integration` keywords to the `keywords` field in `package.json`. - -Only plugins that integrate third-party services are listed in the Medusa integrations page. If your plugin doesn't integrate a third-party service, you can skip this step. - -```json title="package.json" -{ - "keywords": [ - "medusa-plugin-integration", - "medusa-v2" - ], - // ... -} -``` - -In addition, make sure to use one of the following keywords based on your integration type: - -|Keyword|Description|Example| -|---|---|---| -|\`medusa-plugin-analytics\`|Analytics service integration|Google Analytics| -|\`medusa-plugin-auth\`|Authentication service integration|Auth0| -|\`medusa-plugin-cms\`|CMS service integration|Contentful| -|\`medusa-plugin-notification\`|Notification service integration|Twilio SMS| -|\`medusa-plugin-payment\`|Payment service integration|PayPal| -|\`medusa-plugin-search\`|Search service integration|MeiliSearch| -|\`medusa-plugin-shipping\`|Shipping service integration|DHL| -|\`medusa-plugin-other\`|Other third-party integrations|Sentry| - -### Package Dependencies - -Your plugin project will already have the dependencies mentioned in this section. Unless you made changes to the dependencies, you can skip this section. - -In the `package.json` file you must have the Medusa dependencies as `devDependencies` and `peerDependencies`. In addition, you must have `@swc/core` as a `devDependency`, as it's used by the plugin CLI tools. - -For example, assuming `2.5.0` is the latest Medusa version: - -```json title="package.json" -{ - "devDependencies": { - "@medusajs/admin-sdk": "2.5.0", - "@medusajs/cli": "2.5.0", - "@medusajs/framework": "2.5.0", - "@medusajs/medusa": "2.5.0", - "@medusajs/test-utils": "2.5.0", - "@medusajs/ui": "4.0.4", - "@medusajs/icons": "2.5.0", - "@swc/core": "1.5.7", - }, - "peerDependencies": { - "@medusajs/admin-sdk": "2.5.0", - "@medusajs/cli": "2.5.0", - "@medusajs/framework": "2.5.0", - "@medusajs/test-utils": "2.5.0", - "@medusajs/medusa": "2.5.0", - "@medusajs/ui": "4.0.3", - "@medusajs/icons": "2.5.0", - } -} -``` - -### Package Exports - -Your plugin project will already have the exports mentioned in this section. Unless you made changes to the exports or you created your plugin before [Medusa v2.7.0](https://github.com/medusajs/medusa/releases/tag/v2.7.0), you can skip this section. - -In the `package.json` file, make sure your plugin has the following exports: - -```json title="package.json" -{ - "exports": { - "./package.json": "./package.json", - "./workflows": "./.medusa/server/src/workflows/index.js", - "./.medusa/server/src/modules/*": "./.medusa/server/src/modules/*/index.js", - "./providers/*": "./.medusa/server/src/providers/*/index.js", - "./admin": { - "import": "./.medusa/server/src/admin/index.mjs", - "require": "./.medusa/server/src/admin/index.js", - "default": "./.medusa/server/src/admin/index.js" - }, - "./*": "./.medusa/server/src/*.js" - } -} -``` - -Aside from the `./package.json`, `./providers`, and `./admin`, these exports are only a recommendation. You can cherry-pick the files and directories you want to export. - -The plugin exports the following files and directories: - -- `./package.json`: The `package.json` file. Medusa needs to access the `package.json` when registering the plugin. -- `./workflows`: The workflows exported in `./src/workflows/index.ts`. -- `./.medusa/server/src/modules/*`: The definition file of modules. This is useful if you create links to the plugin's modules in the Medusa application. -- `./providers/*`: The definition file of module providers. This is useful if your plugin includes a module provider, allowing you to register the plugin's providers in Medusa applications. Learn more in the [Create Module Providers](#create-module-providers) section. -- `./admin`: The admin extensions exported in `./src/admin/index.ts`. -- `./*`: Any other files in the plugin's `src` directory. - -*** - -## 3. Publish Plugin Locally for Development and Testing - -Medusa's CLI tool provides commands to simplify developing and testing your plugin in a local Medusa application. You start by publishing your plugin in the local package registry, then install it in your Medusa application. You can then watch for changes in the plugin as you develop it. - -### Publish and Install Local Package - -### Prerequisites - -- [Medusa application installed.](https://docs.medusajs.com/learn/installation/index.html.md) - -The first time you create your plugin, you need to publish the package into a local package registry, then install it in your Medusa application. This is a one-time only process. - -To publish the plugin to the local registry, run the following command in your plugin project: - -```bash title="Plugin project" -npx medusa plugin:publish -``` - -This command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. The plugin is published locally under the name you specified in `package.json`. - -Next, navigate to your Medusa application: - -```bash title="Medusa application" -cd ~/path/to/medusa-app -``` - -Make sure to replace `~/path/to/medusa-app` with the path to your Medusa application. - -Then, if your project was created before v2.3.1 of Medusa, make sure to install `yalc` as a development dependency: - -```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green" -npm install --save-dev yalc -``` - -After that, run the following Medusa CLI command to install the plugin: - -```bash title="Medusa application" -npx medusa plugin:add @myorg/plugin-name -``` - -Make sure to replace `@myorg/plugin-name` with the name of your plugin as specified in `package.json`. Your plugin will be installed from the local package registry into your Medusa application. - -### Register Plugin in Medusa Application - -After installing the plugin, you need to register it in your Medusa application in the configurations defined in `medusa-config.ts`. - -Add the plugin to the `plugins` array in the `medusa-config.ts` file: - -```ts title="medusa-config.ts" highlights={pluginHighlights} -module.exports = defineConfig({ - // ... - plugins: [ - { - resolve: "@myorg/plugin-name", - options: {}, - }, - ], -}) -``` - -The `plugins` configuration is an array of objects where each object has a `resolve` key whose value is the name of the plugin package. - -#### Pass Module Options through Plugin - -Each plugin configuration also accepts an `options` property, whose value is an object of options to pass to the plugin's modules. - -For example: - -```ts title="medusa-config.ts" highlights={pluginOptionsHighlight} -module.exports = defineConfig({ - // ... - plugins: [ - { - resolve: "@myorg/plugin-name", - options: { - apiKey: true, - }, - }, - ], -}) -``` - -The `options` property in the plugin configuration is passed to all modules in the plugin. Learn more about module options in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/options/index.html.md). - -### Watch Plugin Changes During Development - -While developing your plugin, you can watch for changes in the plugin and automatically update the plugin in the Medusa application using it. This is the only command you'll continuously need during your plugin development. - -To do that, run the following command in your plugin project: - -```bash title="Plugin project" -npx medusa plugin:develop -``` - -This command will: - -- Watch for changes in the plugin. Whenever a file is changed, the plugin is automatically built. -- Publish the plugin changes to the local package registry. This will automatically update the plugin in the Medusa application using it. You can also benefit from real-time HMR updates of admin extensions. - -### Start Medusa Application - -You can start your Medusa application's development server to test out your plugin: - -```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green" -npm run dev -``` - -While your Medusa application is running and the plugin is being watched, you can test your plugin while developing it in the Medusa application. - -*** - -## 4. Create Customizations in the Plugin - -You can now build your plugin's customizations. The following guide explains how to build different customizations in your plugin. - -- [Create a module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) -- [Create a module link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) -- [Create a workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) -- [Add a workflow hook](https://docs.medusajs.com/learn/fundamentals/workflows/add-workflow-hook/index.html.md) -- [Create an API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) -- [Add a subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) -- [Add a scheduled job](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) -- [Add an admin widget](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md) -- [Add an admin UI route](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md) - -While building those customizations, you can test them in your Medusa application by [watching the plugin changes](#watch-plugin-changes-during-development) and [starting the Medusa application](#start-medusa-application). - -### Generating Migrations for Modules - -During your development, you may need to generate migrations for modules in your plugin. To do that, first, add the following environment variables in your plugin project: - -```plain title="Plugin project" -DB_USERNAME=postgres -DB_PASSWORD=123... -DB_HOST=localhost -DB_PORT=5432 -DB_NAME=db_name -``` - -You can add these environment variables in a `.env` file in your plugin project. The variables are: - -- `DB_USERNAME`: The username of the PostgreSQL user to connect to the database. -- `DB_PASSWORD`: The password of the PostgreSQL user to connect to the database. -- `DB_HOST`: The host of the PostgreSQL database. Typically, it's `localhost` if you're running the database locally. -- `DB_PORT`: The port of the PostgreSQL database. Typically, it's `5432` if you're running the database locally. -- `DB_NAME`: The name of the PostgreSQL database to connect to. - -Then, run the following command in your plugin project to generate migrations for the modules in your plugin: - -```bash title="Plugin project" -npx medusa plugin:db:generate -``` - -This command generates migrations for all modules in the plugin. - -Finally, run these migrations on the Medusa application that the plugin is installed in using the `db:migrate` command: - -```bash title="Medusa application" -npx medusa db:migrate -``` - -The migrations in your application, including your plugin, will run and update the database. - -### Importing Module Resources - -In the [Prepare Plugin](#2-prepare-plugin) section, you learned about [exported resources](#package-exports) in your plugin. - -These exports allow you to import your plugin resources in your Medusa application, including workflows, links and modules. - -For example, to import the plugin's workflow in your Medusa application: - -`@myorg/plugin-name` is the plugin package's name. - -```ts -import { Workflow1, Workflow2 } from "@myorg/plugin-name/workflows" -import BlogModule from "@myorg/plugin-name/modules/blog" -// import other files created in plugin like ./src/types/blog.ts -import BlogType from "@myorg/plugin-name/types/blog" -``` - -### Create Module Providers - -The [exported resources](#package-exports) also allow you to import module providers in your plugin and register them in the Medusa application's configuration. A module provider is a module that provides the underlying logic or integration related to a commerce or Infrastructure Module. - -For example, assuming your plugin has a [Notification Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/notification/index.html.md) called `my-notification`, you can register it in your Medusa application's configuration like this: - -`@myorg/plugin-name` is the plugin package's name. - -```ts highlights={[["9"]]} title="medusa-config.ts" -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/notification", - options: { - providers: [ - { - resolve: "@myorg/plugin-name/providers/my-notification", - id: "my-notification", - options: { - channels: ["email"], - // provider options... - }, - }, - ], - }, - }, - ], -}) -``` - -You pass to `resolve` the path to the provider relative to the plugin package. So, in this example, the `my-notification` provider is located in `./src/providers/my-notification/index.ts` of the plugin. - -To learn how to create module providers, refer to the following guides: - -- [File Module Provider](https://docs.medusajs.com/resources/references/file-provider-module/index.html.md) -- [Notification Module Provider](https://docs.medusajs.com/resources/references/notification-provider-module/index.html.md) -- [Auth Module Provider](https://docs.medusajs.com/resources/references/auth/provider/index.html.md) -- [Payment Module Provider](https://docs.medusajs.com/resources/references/payment/provider/index.html.md) -- [Fulfillment Module Provider](https://docs.medusajs.com/resources/references/fulfillment/provider/index.html.md) -- [Tax Module Provider](https://docs.medusajs.com/resources/references/tax/provider/index.html.md) - -*** - -## 5. Publish Plugin to NPM - -Make sure to add the keywords mentioned in the [Package Keywords](#package-keywords) section in your plugin's `package.json` file. - -Medusa's CLI tool provides a command that bundles your plugin to be published to npm. Once you're ready to publish your plugin publicly, run the following command in your plugin project: - -```bash -npx medusa plugin:build -``` - -The command will compile an output in the `.medusa/server` directory. - -You can now publish the plugin to npm using the [NPM CLI tool](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). Run the following command to publish the plugin to npm: - -```bash -npm publish -``` - -If you haven't logged in before with your NPM account, you'll be asked to log in first. Then, your package is published publicly to be used in any Medusa application. - -### Install Public Plugin in Medusa Application - -You install a plugin that's published publicly using your package manager. For example: - -```bash npm2yarn -npm install @myorg/plugin-name -``` - -Where `@myorg/plugin-name` is the name of your plugin as published on NPM. - -Then, register the plugin in your Medusa application's configurations as explained in [this section](#register-plugin-in-medusa-application). - -*** - -## Update a Published Plugin - -To update the Medusa dependencies in a plugin, refer to [this documentation](https://docs.medusajs.com/learn/update#update-plugin-project/index.html.md). - -If you've published a plugin and you've made changes to it, you'll have to publish the update to NPM again. - -First, run the following command to change the version of the plugin: - -```bash -npm version -``` - -Where `` indicates the type of version update you’re publishing. For example, it can be `major` or `minor`. Refer to the [npm version documentation](https://docs.npmjs.com/cli/v10/commands/npm-version) for more information. - -Then, re-run the same commands for publishing a plugin: - -```bash -npx medusa plugin:build -npm publish -``` - -This will publish an updated version of your plugin under a new version. - - -# Scheduled Jobs Number of Executions - -In this chapter, you'll learn how to set a limit on the number of times a scheduled job is executed. - -## numberOfExecutions Option - -The export configuration object of the scheduled job accepts an optional property `numberOfExecutions`. Its value is a number indicating how many times the scheduled job can be executed during the Medusa application's runtime. - -For example: - -```ts highlights={highlights} -export default async function myCustomJob() { - console.log("I'll be executed three times only.") -} - -export const config = { - name: "hello-world", - // execute every minute - schedule: "* * * * *", - numberOfExecutions: 3, -} -``` - -The above scheduled job has the `numberOfExecutions` configuration set to `3`. - -So, it'll only execute 3 times, each every minute, then it won't be executed anymore. - -If you restart the Medusa application, the scheduled job will be executed again until reaching the number of executions specified. - - -# 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. - - -# Expose a Workflow Hook - -In this chapter, you'll learn how to expose a hook in your workflow. - -## When to Expose a Hook - -Your workflow is reusable in other applications, and you allow performing an external action at some point in your workflow. - -Your workflow isn't reusable by other applications. Use a step that performs what a hook handler would instead. - -*** - -## How to Expose a Hook in a Workflow? - -To expose a hook in your workflow, use `createHook` from the Workflows SDK. - -For example: - -```ts title="src/workflows/my-workflow/index.ts" highlights={hookHighlights} -import { - createStep, - createHook, - createWorkflow, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" -import { createProductStep } from "./steps/create-product" - -export const myWorkflow = createWorkflow( - "my-workflow", - function (input) { - const product = createProductStep(input) - const productCreatedHook = createHook( - "productCreated", - { productId: product.id } - ) - - return new WorkflowResponse(product, { - hooks: [productCreatedHook], - }) - } -) -``` - -The `createHook` function accepts two parameters: - -1. The first is a string indicating the hook's name. You use this to consume the hook later. -2. The second is the input to pass to the hook handler. - -The workflow must also pass an object having a `hooks` property as a second parameter to the `WorkflowResponse` constructor. Its value is an array of the workflow's hooks. - -### How to Consume the Hook? - -To consume the hook of the workflow, create the file `src/workflows/hooks/my-workflow.ts` with the following content: - -```ts title="src/workflows/hooks/my-workflow.ts" highlights={handlerHighlights} -import { myWorkflow } from "../my-workflow" - -myWorkflow.hooks.productCreated( - async ({ productId }, { container }) => { - // TODO perform an action - } -) -``` - -The hook is available on the workflow's `hooks` property using its name `productCreated`. - -You invoke the hook, passing a step function (the hook handler) as a parameter. - - -# Compensation Function - -In this chapter, you'll learn what a compensation function is and how to add it to a step. - -## What is a Compensation Function - -A compensation function rolls back or undoes changes made by a step when an error occurs in the workflow. - -For example, if a step creates a record, the compensation function deletes the record when an error occurs later in the workflow. - -By using compensation functions, you provide a mechanism that guarantees data consistency in your application and across systems. - -*** - -## How to add a Compensation Function? - -A compensation function is passed as a second parameter to the `createStep` function. - -For example, create the file `src/workflows/hello-world.ts` with the following content: - -```ts title="src/workflows/hello-world.ts" highlights={[["15"], ["16"], ["17"]]} collapsibleLines="1-5" expandButtonLabel="Show Imports" -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" - -const step1 = createStep( - "step-1", - async () => { - const message = `Hello from step one!` - - console.log(message) - - return new StepResponse(message) - }, - async () => { - console.log("Oops! Rolling back my changes...") - } -) -``` - -Each step can have a compensation function. The compensation function only runs if an error occurs throughout the workflow. - -*** - -## Test the Compensation Function - -Create a step in the same `src/workflows/hello-world.ts` file that throws an error: - -```ts title="src/workflows/hello-world.ts" -const step2 = createStep( - "step-2", - async () => { - throw new Error("Throwing an error...") - } -) -``` - -Then, create a workflow that uses the steps: - -```ts title="src/workflows/hello-world.ts" collapsibleLines="1-8" expandButtonLabel="Show Imports" -import { - createWorkflow, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" -// other imports... - -// steps... - -const myWorkflow = createWorkflow( - "hello-world", - function (input) { - const str1 = step1() - step2() - - return new WorkflowResponse({ - message: str1, - }) -}) - -export default myWorkflow -``` - -Finally, execute the workflow from an API route: - -```ts title="src/api/workflow/route.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import myWorkflow from "../../../workflows/hello-world" - -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await myWorkflow(req.scope) - .run() - - res.send(result) -} -``` - -Run the Medusa application and send a `GET` request to `/workflow`: - -```bash -curl http://localhost:9000/workflow -``` - -In the console, you'll see: - -- `Hello from step one!` logged in the terminal, indicating that the first step ran successfully. -- `Oops! Rolling back my changes...` logged in the terminal, indicating that the second step failed and the compensation function of the first step ran consequently. - -*** - -## Pass Input to Compensation Function - -If a step creates a record, the compensation function must receive the ID of the record to remove it. - -To pass input to the compensation function, pass a second parameter in the `StepResponse` returned by the step. - -For example: - -```ts highlights={inputHighlights} -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" - -const step1 = createStep( - "step-1", - async () => { - return new StepResponse( - `Hello from step one!`, - { message: "Oops! Rolling back my changes..." } - ) - }, - async ({ message }) => { - console.log(message) - } -) -``` - -In this example, the step passes an object as a second parameter to `StepResponse`. - -The compensation function receives the object and uses its `message` property to log a message. - -*** - -## Resolve Resources from the Medusa Container - -The compensation function receives an object second parameter. The object has a `container` property that you use to resolve resources from the Medusa container. - -For example: - -```ts -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { ContainerRegistrationKeys } from "@medusajs/framework/utils" - -const step1 = createStep( - "step-1", - async () => { - return new StepResponse( - `Hello from step one!`, - { message: "Oops! Rolling back my changes..." } - ) - }, - async ({ message }, { container }) => { - const logger = container.resolve( - ContainerRegistrationKeys.LOGGER - ) - - logger.info(message) - } -) -``` - -In this example, you use the `container` property in the second object parameter of the compensation function to resolve the logger. - -You then use the logger to log a message. - -*** - -## Handle Step Errors in Loops - -This feature is only available after [Medusa v2.0.5](https://github.com/medusajs/medusa/releases/tag/v2.0.5). - -Consider you have a module that integrates a third-party ERP system, and you're creating a workflow that deletes items in that ERP. You may have the following step: - -```ts -// other imports... -import { promiseAll } from "@medusajs/framework/utils" - -type StepInput = { - ids: string[] -} - -const step1 = createStep( - "step-1", - async ({ ids }: StepInput, { container }) => { - const erpModuleService = container.resolve( - ERP_MODULE - ) - const prevData: unknown[] = [] - - await promiseAll( - ids.map(async (id) => { - const data = await erpModuleService.retrieve(id) - - await erpModuleService.delete(id) - - prevData.push(id) - }) - ) - - return new StepResponse(ids, prevData) - } -) -``` - -In the step, you loop over the IDs to retrieve the item's data, store them in a `prevData` variable, then delete them using the ERP Module's service. You then pass the `prevData` variable to the compensation function. - -However, if an error occurs in the loop, the `prevData` variable won't be passed to the compensation function as the execution never reached the return statement. - -To handle errors in the loop so that the compensation function receives the last version of `prevData` before the error occurred, you wrap the loop in a try-catch block. Then, in the catch block, you invoke and return the `StepResponse.permanentFailure` function: - -```ts highlights={highlights} -try { - await promiseAll( - ids.map(async (id) => { - const data = await erpModuleService.retrieve(id) - - await erpModuleService.delete(id) - - prevData.push(id) - }) - ) -} catch (e) { - return StepResponse.permanentFailure( - `An error occurred: ${e}`, - prevData - ) -} -``` - -The `StepResponse.permanentFailure` fails the step and its workflow, triggering current and previous steps' compensation functions. The `permanentFailure` function accepts as a first parameter the error message, which is saved in the workflow's error details, and as a second parameter the data to pass to the compensation function. - -So, if an error occurs during the loop, the compensation function will still receive the `prevData` variable to undo the changes made before the step failed. - -For more details on error handling in workflows and steps, check the [Handling Errors chapter](https://docs.medusajs.com/learn/fundamentals/workflows/errors/index.html.md). - - -# 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. - - # Loaders In this chapter, you’ll learn about loaders and how to use them. @@ -13997,6 +14628,861 @@ 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. + +## Module's Main and Internal Services + +A module has one main service only, which is the service exported in the module's definition. + +However, you may use other services in your module to better organize your code or split functionalities. These are called internal services that can be resolved within your module, but not in external resources. + +*** + +## How to Add an Internal Service + +### 1. Create Service + +To add an internal service, create it in the `services` directory of your module. + +For example, create the file `src/modules/blog/services/client.ts` with the following content: + +```ts title="src/modules/blog/services/client.ts" +export class ClientService { + async getMessage(): Promise { + return "Hello, World!" + } +} +``` + +### 2. Export Service in Index + +Next, create an `index.ts` file under the `services` directory of the module that exports your internal services. + +For example, create the file `src/modules/blog/services/index.ts` with the following content: + +```ts title="src/modules/blog/services/index.ts" +export * from "./client" +``` + +This exports the `ClientService`. + +### 3. Resolve Internal Service + +Internal services exported in the `services/index.ts` file of your module are now registered in the container and can be resolved in other services in the module as well as loaders. + +For example, in your main service: + +```ts title="src/modules/blog/service.ts" highlights={[["5"], ["13"]]} +// other imports... +import { ClientService } from "./services" + +type InjectedDependencies = { + clientService: ClientService +} + +class BlogModuleService extends MedusaService({ + Post, +}){ + protected clientService_: ClientService + + constructor({ clientService }: InjectedDependencies) { + super(...arguments) + this.clientService_ = clientService + } +} +``` + +You can now use your internal service in your main service. + +*** + +## Resolve Resources in Internal Service + +Resolve dependencies from your module's container in the constructor of your internal service. + +For example: + +```ts +import { Logger } from "@medusajs/framework/types" + +type InjectedDependencies = { + logger: Logger +} + +export class ClientService { + protected logger_: Logger + + constructor({ logger }: InjectedDependencies) { + this.logger_ = logger + } +} +``` + +*** + +## Access Module Options + +Your internal service can't access the module's options. + +To retrieve the module's options, use the `configModule` registered in the module's container, which is the configurations in `medusa-config.ts`. + +For example: + +```ts +import { ConfigModule } from "@medusajs/framework/types" +import { BLOG_MODULE } from ".." + +export type InjectedDependencies = { + configModule: ConfigModule +} + +export class ClientService { + protected options: Record + + constructor({ configModule }: InjectedDependencies) { + const moduleDef = configModule.modules[BLOG_MODULE] + + if (typeof moduleDef !== "boolean") { + this.options = moduleDef.options + } + } +} +``` + +The `configModule` has a `modules` property that includes all registered modules. Retrieve the module's configuration using its registration key. + +If its value is not a `boolean`, set the service's options to the module configuration's `options` property. + + +# Service Factory + +In this chapter, you’ll learn about what the service factory is and how to use it. + +## What is the Service Factory? + +Medusa provides a service factory that your module’s main service can extend. + +The service factory generates data management methods for your data models in the database, so you don't have to implement these methods manually. + +Your service provides data-management functionalities of your data models. + +*** + +## How to Extend the Service Factory? + +Medusa provides the service factory as a `MedusaService` function your service extends. The function creates and returns a service class with generated data-management methods. + +For example, create the file `src/modules/blog/service.ts` with the following content: + +```ts title="src/modules/blog/service.ts" highlights={highlights} +import { MedusaService } from "@medusajs/framework/utils" +import Post from "./models/post" + +class BlogModuleService extends MedusaService({ + Post, +}){ + // TODO implement custom methods +} + +export default BlogModuleService +``` + +### MedusaService Parameters + +The `MedusaService` function accepts one parameter, which is an object of data models to generate data-management methods for. + +In the example above, since the `BlogModuleService` extends `MedusaService`, it has methods to manage the `Post` data model, such as `createPosts`. + +### Generated Methods + +The service factory generates methods to manage the records of each of the data models provided in the first parameter in the database. + +The method's names are the operation's name, suffixed by the data model's key in the object parameter passed to `MedusaService`. + +For example, the following methods are generated for the service above: + +Find a complete reference of each of the methods in [this documentation](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) + +### listPosts + +### listPosts + +This method retrieves an array of records based on filters and pagination configurations. + +For example: + +```ts +const posts = await blogModuleService + .listPosts() + +// with filters +const posts = await blogModuleService + .listPosts({ + id: ["123"] + }) +``` + +### listAndCountPosts + +### retrievePost + +This method retrieves a record by its ID. + +For example: + +```ts +const post = await blogModuleService + .retrievePost("123") +``` + +### retrievePost + +### updatePosts + +This method updates and retrieves records of the data model. + +For example: + +```ts +const post = await blogModuleService + .updatePosts({ + id: "123", + title: "test" + }) + +// update multiple +const posts = await blogModuleService + .updatePosts([ + { + id: "123", + title: "test" + }, + { + id: "321", + title: "test 2" + }, + ]) + +// use filters +const posts = await blogModuleService + .updatePosts([ + { + selector: { + id: ["123", "321"] + }, + data: { + title: "test" + } + }, + ]) +``` + +### createPosts + +### softDeletePosts + +This method soft-deletes records using an array of IDs or an object of filters. + +For example: + +```ts +await blogModuleService.softDeletePosts("123") + +// soft-delete multiple +await blogModuleService.softDeletePosts([ + "123", "321" +]) + +// use filters +await blogModuleService.softDeletePosts({ + id: ["123", "321"] +}) +``` + +### updatePosts + +### deletePosts + +### softDeletePosts + +### restorePosts + +### Using a Constructor + +If you implement the `constructor` of your service, make sure to call `super` passing it `...arguments`. + +For example: + +```ts highlights={[["8"]]} +import { MedusaService } from "@medusajs/framework/utils" +import Post from "./models/post" + +class BlogModuleService extends MedusaService({ + Post, +}){ + constructor() { + super(...arguments) + } +} + +export default BlogModuleService +``` + + +# Module Options + +In this chapter, you’ll learn about passing options to your module from the Medusa application’s configurations and using them in the module’s resources. + +## What are Module Options? + +A module can receive options to customize or configure its functionality. For example, if you’re creating a module that integrates a third-party service, you’ll want to receive the integration credentials in the options rather than adding them directly in your code. + +*** + +## How to Pass Options to a Module? + +To pass options to a module, add an `options` property to the module’s configuration in `medusa-config.ts`. + +For example: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "./src/modules/blog", + options: { + capitalize: true, + }, + }, + ], +}) +``` + +The `options` property’s value is an object. You can pass any properties you want. + +### Pass Options to a Module in a Plugin + +If your module is part of a plugin, you can pass options to the module in the plugin’s configuration. + +For example: + +```ts title="medusa-config.ts" +import { defineConfig } from "@medusajs/framework/utils" +module.exports = defineConfig({ + plugins: [ + { + resolve: "@myorg/plugin-name", + options: { + capitalize: true, + }, + }, + ], +}) +``` + +The `options` property in the plugin configuration is passed to all modules in a plugin. + +*** + +## Access Module Options in Main Service + +The module’s main service receives the module options as a second parameter. + +For example: + +```ts title="src/modules/blog/service.ts" highlights={[["12"], ["14", "options?: ModuleOptions"], ["17"], ["18"], ["19"]]} +import { MedusaService } from "@medusajs/framework/utils" +import Post from "./models/post" + +// recommended to define type in another file +type ModuleOptions = { + capitalize?: boolean +} + +export default class BlogModuleService extends MedusaService({ + Post, +}){ + protected options_: ModuleOptions + + constructor({}, options?: ModuleOptions) { + super(...arguments) + + this.options_ = options || { + capitalize: false, + } + } + + // ... +} +``` + +*** + +## Access Module Options in Loader + +The object that a module’s loaders receive as a parameter has an `options` property holding the module's options. + +For example: + +```ts title="src/modules/blog/loaders/hello-world.ts" highlights={[["11"], ["12", "ModuleOptions", "The type of expected module options."], ["16"]]} +import { + LoaderOptions, +} from "@medusajs/framework/types" + +// recommended to define type in another file +type ModuleOptions = { + capitalize?: boolean +} + +export default async function helloWorldLoader({ + options, +}: LoaderOptions) { + + console.log( + "[BLOG MODULE] Just started the Medusa application!", + options + ) +} +``` + +*** + +## Validate Module Options + +If you expect a certain option and want to throw an error if it's not provided or isn't valid, it's recommended to perform the validation in a loader. The module's service is only instantiated when it's used, whereas the loader runs the when the Medusa application starts. + +So, by performing the validation in the loader, you ensure you can throw an error at an early point, rather than when the module is used. + +For example, to validate that the Hello Module received an `apiKey` option, create the loader `src/modules/loaders/validate.ts`: + +```ts title="src/modules/blog/loaders/validate.ts" +import { LoaderOptions } from "@medusajs/framework/types" +import { MedusaError } from "@medusajs/framework/utils" + +// recommended to define type in another file +type ModuleOptions = { + apiKey?: string +} + +export default async function validationLoader({ + options, +}: LoaderOptions) { + if (!options.apiKey) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Hello Module requires an apiKey option." + ) + } +} +``` + +Then, export the loader in the module's definition file, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/loaders/index.html.md): + +```ts title="src/modules/blog/index.ts" +// other imports... +import validationLoader from "./loaders/validate" + +export const BLOG_MODULE = "blog" + +export default Module(BLOG_MODULE, { + // ... + loaders: [validationLoader], +}) +``` + +Now, when the Medusa application starts, the loader will run, validating the module's options and throwing an error if the `apiKey` option is missing. + + +# Expose a Workflow Hook + +In this chapter, you'll learn how to expose a hook in your workflow. + +## When to Expose a Hook + +Your workflow is reusable in other applications, and you allow performing an external action at some point in your workflow. + +Your workflow isn't reusable by other applications. Use a step that performs what a hook handler would instead. + +*** + +## How to Expose a Hook in a Workflow? + +To expose a hook in your workflow, use `createHook` from the Workflows SDK. + +For example: + +```ts title="src/workflows/my-workflow/index.ts" highlights={hookHighlights} +import { + createStep, + createHook, + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { createProductStep } from "./steps/create-product" + +export const myWorkflow = createWorkflow( + "my-workflow", + function (input) { + const product = createProductStep(input) + const productCreatedHook = createHook( + "productCreated", + { productId: product.id } + ) + + return new WorkflowResponse(product, { + hooks: [productCreatedHook], + }) + } +) +``` + +The `createHook` function accepts two parameters: + +1. The first is a string indicating the hook's name. You use this to consume the hook later. +2. The second is the input to pass to the hook handler. + +The workflow must also pass an object having a `hooks` property as a second parameter to the `WorkflowResponse` constructor. Its value is an array of the workflow's hooks. + +### How to Consume the Hook? + +To consume the hook of the workflow, create the file `src/workflows/hooks/my-workflow.ts` with the following content: + +```ts title="src/workflows/hooks/my-workflow.ts" highlights={handlerHighlights} +import { myWorkflow } from "../my-workflow" + +myWorkflow.hooks.productCreated( + async ({ productId }, { container }) => { + // TODO perform an action + } +) +``` + +The hook is available on the workflow's `hooks` property using its name `productCreated`. + +You invoke the hook, passing a step function (the hook handler) as a parameter. + + +# 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. @@ -14156,6 +15642,262 @@ Since `then` returns a value different than the step's result, you pass to the ` The second and third parameters are the same as the parameters you previously passed to `when`. +# Compensation Function + +In this chapter, you'll learn what a compensation function is and how to add it to a step. + +## What is a Compensation Function + +A compensation function rolls back or undoes changes made by a step when an error occurs in the workflow. + +For example, if a step creates a record, the compensation function deletes the record when an error occurs later in the workflow. + +By using compensation functions, you provide a mechanism that guarantees data consistency in your application and across systems. + +*** + +## How to add a Compensation Function? + +A compensation function is passed as a second parameter to the `createStep` function. + +For example, create the file `src/workflows/hello-world.ts` with the following content: + +```ts title="src/workflows/hello-world.ts" highlights={[["15"], ["16"], ["17"]]} collapsibleLines="1-5" expandButtonLabel="Show Imports" +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + "step-1", + async () => { + const message = `Hello from step one!` + + console.log(message) + + return new StepResponse(message) + }, + async () => { + console.log("Oops! Rolling back my changes...") + } +) +``` + +Each step can have a compensation function. The compensation function only runs if an error occurs throughout the workflow. + +*** + +## Test the Compensation Function + +Create a step in the same `src/workflows/hello-world.ts` file that throws an error: + +```ts title="src/workflows/hello-world.ts" +const step2 = createStep( + "step-2", + async () => { + throw new Error("Throwing an error...") + } +) +``` + +Then, create a workflow that uses the steps: + +```ts title="src/workflows/hello-world.ts" collapsibleLines="1-8" expandButtonLabel="Show Imports" +import { + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +// other imports... + +// steps... + +const myWorkflow = createWorkflow( + "hello-world", + function (input) { + const str1 = step1() + step2() + + return new WorkflowResponse({ + message: str1, + }) +}) + +export default myWorkflow +``` + +Finally, execute the workflow from an API route: + +```ts title="src/api/workflow/route.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import myWorkflow from "../../../workflows/hello-world" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await myWorkflow(req.scope) + .run() + + res.send(result) +} +``` + +Run the Medusa application and send a `GET` request to `/workflow`: + +```bash +curl http://localhost:9000/workflow +``` + +In the console, you'll see: + +- `Hello from step one!` logged in the terminal, indicating that the first step ran successfully. +- `Oops! Rolling back my changes...` logged in the terminal, indicating that the second step failed and the compensation function of the first step ran consequently. + +*** + +## Pass Input to Compensation Function + +If a step creates a record, the compensation function must receive the ID of the record to remove it. + +To pass input to the compensation function, pass a second parameter in the `StepResponse` returned by the step. + +For example: + +```ts highlights={inputHighlights} +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + "step-1", + async () => { + return new StepResponse( + `Hello from step one!`, + { message: "Oops! Rolling back my changes..." } + ) + }, + async ({ message }) => { + console.log(message) + } +) +``` + +In this example, the step passes an object as a second parameter to `StepResponse`. + +The compensation function receives the object and uses its `message` property to log a message. + +*** + +## Resolve Resources from the Medusa Container + +The compensation function receives an object second parameter. The object has a `container` property that you use to resolve resources from the Medusa container. + +For example: + +```ts +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" + +const step1 = createStep( + "step-1", + async () => { + return new StepResponse( + `Hello from step one!`, + { message: "Oops! Rolling back my changes..." } + ) + }, + async ({ message }, { container }) => { + const logger = container.resolve( + ContainerRegistrationKeys.LOGGER + ) + + logger.info(message) + } +) +``` + +In this example, you use the `container` property in the second object parameter of the compensation function to resolve the logger. + +You then use the logger to log a message. + +*** + +## Handle Step Errors in Loops + +This feature is only available after [Medusa v2.0.5](https://github.com/medusajs/medusa/releases/tag/v2.0.5). + +Consider you have a module that integrates a third-party ERP system, and you're creating a workflow that deletes items in that ERP. You may have the following step: + +```ts +// other imports... +import { promiseAll } from "@medusajs/framework/utils" + +type StepInput = { + ids: string[] +} + +const step1 = createStep( + "step-1", + async ({ ids }: StepInput, { container }) => { + const erpModuleService = container.resolve( + ERP_MODULE + ) + const prevData: unknown[] = [] + + await promiseAll( + ids.map(async (id) => { + const data = await erpModuleService.retrieve(id) + + await erpModuleService.delete(id) + + prevData.push(id) + }) + ) + + return new StepResponse(ids, prevData) + } +) +``` + +In the step, you loop over the IDs to retrieve the item's data, store them in a `prevData` variable, then delete them using the ERP Module's service. You then pass the `prevData` variable to the compensation function. + +However, if an error occurs in the loop, the `prevData` variable won't be passed to the compensation function as the execution never reached the return statement. + +To handle errors in the loop so that the compensation function receives the last version of `prevData` before the error occurred, you wrap the loop in a try-catch block. Then, in the catch block, you invoke and return the `StepResponse.permanentFailure` function: + +```ts highlights={highlights} +try { + await promiseAll( + ids.map(async (id) => { + const data = await erpModuleService.retrieve(id) + + await erpModuleService.delete(id) + + prevData.push(id) + }) + ) +} catch (e) { + return StepResponse.permanentFailure( + `An error occurred: ${e}`, + prevData + ) +} +``` + +The `StepResponse.permanentFailure` fails the step and its workflow, triggering current and previous steps' compensation functions. The `permanentFailure` function accepts as a first parameter the error message, which is saved in the workflow's error details, and as a second parameter the data to pass to the compensation function. + +So, if an error occurs during the loop, the compensation function will still receive the `prevData` variable to undo the changes made before the step failed. + +For more details on error handling in workflows and steps, check the [Handling Errors chapter](https://docs.medusajs.com/learn/fundamentals/workflows/errors/index.html.md). + + # Workflow Constraints This chapter lists constraints of defining a workflow or its steps. @@ -14510,136 +16252,6 @@ const step1 = createStep( ``` -# Execute Another Workflow - -In this chapter, you'll learn how to execute a workflow in another. - -## Execute in a Workflow - -To execute a workflow in another, use the `runAsStep` method that every workflow has. - -For example: - -```ts highlights={workflowsHighlights} collapsibleLines="1-7" expandMoreButton="Show Imports" -import { - createWorkflow, -} from "@medusajs/framework/workflows-sdk" -import { - createProductsWorkflow, -} from "@medusajs/medusa/core-flows" - -const workflow = createWorkflow( - "hello-world", - async (input) => { - const products = createProductsWorkflow.runAsStep({ - input: { - products: [ - // ... - ], - }, - }) - - // ... - } -) -``` - -Instead of invoking the workflow and passing it the container, you use its `runAsStep` method and pass it an object as a parameter. - -The object has an `input` property to pass input to the workflow. - -*** - -## Preparing Input Data - -If you need to perform some data manipulation to prepare the other workflow's input data, use `transform` from the Workflows SDK. - -Learn about transform in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md). - -For example: - -```ts highlights={transformHighlights} collapsibleLines="1-12" -import { - createWorkflow, - transform, -} from "@medusajs/framework/workflows-sdk" -import { - createProductsWorkflow, -} from "@medusajs/medusa/core-flows" - -type WorkflowInput = { - title: string -} - -const workflow = createWorkflow( - "hello-product", - async (input: WorkflowInput) => { - const createProductsData = transform({ - input, - }, (data) => [ - { - title: `Hello ${data.input.title}`, - }, - ]) - - const products = createProductsWorkflow.runAsStep({ - input: { - products: createProductsData, - }, - }) - - // ... - } -) -``` - -In this example, you use the `transform` function to prepend `Hello` to the title of the product. Then, you pass the result as an input to the `createProductsWorkflow`. - -*** - -## Run Workflow Conditionally - -To run a workflow in another based on a condition, use when-then from the Workflows SDK. - -Learn about when-then in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/conditions/index.html.md). - -For example: - -```ts highlights={whenHighlights} collapsibleLines="1-16" -import { - createWorkflow, - when, -} from "@medusajs/framework/workflows-sdk" -import { - createProductsWorkflow, -} from "@medusajs/medusa/core-flows" -import { - CreateProductWorkflowInputDTO, -} from "@medusajs/framework/types" - -type WorkflowInput = { - product?: CreateProductWorkflowInputDTO - should_create?: boolean -} - -const workflow = createWorkflow( - "hello-product", - async (input: WorkflowInput) => { - const product = when(input, ({ should_create }) => should_create) - .then(() => { - return createProductsWorkflow.runAsStep({ - input: { - products: [input.product], - }, - }) - }) - } -) -``` - -In this example, you use when-then to run the `createProductsWorkflow` only if `should_create` (passed in the `input`) is enabled. - - # 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. @@ -15180,6 +16792,189 @@ 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. @@ -15522,59 +17317,6 @@ if (workflowExecution.state === "failed") { Other state values include `done`, `invoking`, and `compensating`. -# 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`. - - # Workflow Hooks In this chapter, you'll learn what a workflow hook is and how to consume them. @@ -15699,890 +17441,6 @@ export async function POST(req: MedusaRequest, res: MedusaResponse) { Your hook handler then receives that passed data in the `additional_data` object. -# Workflow Timeout - -In this chapter, you’ll learn how to set a timeout for workflows and steps. - -## What is a Workflow Timeout? - -By default, a workflow doesn’t have a timeout. It continues execution until it’s finished or an error occurs. - -You can configure a workflow’s timeout to indicate how long the workflow can execute. If a workflow's execution time passes the configured timeout, it is failed and an error is thrown. - -### Timeout Doesn't Stop Step Execution - -Configuring a timeout doesn't stop the execution of a step in progress. The timeout only affects the status of the workflow and its result. - -*** - -## Configure Workflow Timeout - -The `createWorkflow` function can accept a configuration object instead of the workflow’s name. - -In the configuration object, you pass a `timeout` property, whose value is a number indicating the timeout in seconds. - -For example: - -```ts title="src/workflows/hello-world.ts" highlights={[["16"]]} collapsibleLines="1-13" expandButtonLabel="Show More" -import { - createStep, - createWorkflow, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" - -const step1 = createStep( - "step-1", - async () => { - // ... - } -) - -const myWorkflow = createWorkflow({ - name: "hello-world", - timeout: 2, // 2 seconds -}, function () { - const str1 = step1() - - return new WorkflowResponse({ - message: str1, - }) -}) - -export default myWorkflow - -``` - -This workflow's executions fail if they run longer than two seconds. - -A workflow’s timeout error is returned in the `errors` property of the workflow’s execution, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/errors/index.html.md). The error’s name is `TransactionTimeoutError`. - -*** - -## Configure Step Timeout - -Alternatively, you can configure the timeout for a step rather than the entire workflow. - -As mentioned in the previous section, the timeout doesn't stop the execution of the step. It only affects the step's status and output. - -The step’s configuration object accepts a `timeout` property, whose value is a number indicating the timeout in seconds. - -For example: - -```tsx -const step1 = createStep( - { - name: "step-1", - timeout: 2, // 2 seconds - }, - async () => { - // ... - } -) -``` - -This step's executions fail if they run longer than two seconds. - -A step’s timeout error is returned in the `errors` property of the workflow’s execution, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/errors/index.html.md). The error’s name is `TransactionStepTimeoutError`. - - -# 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: 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. - - # Data Manipulation in Workflows with transform In this chapter, you'll learn how to use `transform` from the Workflows SDK to manipulate data and variables in a workflow. @@ -16788,1166 +17646,90 @@ const myWorkflow = createWorkflow( ``` -# Guide: Schedule Syncing Brands from Third-Party +# Workflow Timeout -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. +In this chapter, you’ll learn how to set a timeout for workflows and steps. -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. +## What is a Workflow Timeout? -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. +By default, a workflow doesn’t have a timeout. It continues execution until it’s finished or an error occurs. -Learn more about scheduled jobs in [this chapter](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md). +You can configure a workflow’s timeout to indicate how long the workflow can execute. If a workflow's execution time passes the configured timeout, it is failed and an error is thrown. -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. +### Timeout Doesn't Stop Step Execution -### Prerequisites - -- [CMS Module](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md) +Configuring a timeout doesn't stop the execution of a step in progress. The timeout only affects the status of the workflow and its result. *** -## 1. Implement Syncing Workflow +## Configure Workflow Timeout -You'll start by implementing the syncing logic in a workflow, then execute the workflow later in the scheduled job. +The `createWorkflow` function can accept a configuration object instead of the workflow’s name. -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: 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: 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. - - -# 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. - - -# Docs Contribution Guidelines - -Thank you for your interest in contributing to the documentation! You will be helping the open source community and other developers interested in learning more about Medusa and using it. - -This guide is specific to contributing to the documentation. If you’re interested in contributing to Medusa’s codebase, check out the [contributing guidelines in the Medusa GitHub repository](https://github.com/medusajs/medusa/blob/develop/CONTRIBUTING.md). - -## What Can You Contribute? - -You can contribute to the Medusa documentation in the following ways: - -- Fixes to existing content. This includes small fixes like typos, or adding missing information. -- Additions to the documentation. If you think a documentation page can be useful to other developers, you can contribute by adding it. - - Make sure to open an issue first in the [medusa repository](https://github.com/medusajs/medusa) to confirm that you can add that documentation page. -- Fixes to UI components and tooling. If you find a bug while browsing the documentation, you can contribute by fixing it. - -*** - -## Documentation Workspace - -Medusa's documentation projects are all part of the documentation yarn workspace, which you can find in the [medusa repository](https://github.com/medusajs/medusa) under the `www` directory. - -The workspace has the following two directories: - -- `apps`: this directory holds the different documentation websites and projects. - - `book`: includes the codebase for the [main Medusa documentation](https://docs.medusajs.com//index.html.md). It's built with [Next.js 15](https://nextjs.org/). - - `resources`: includes the codebase for the resources documentation, which powers different sections of the docs such as the [Integrations](https://docs.medusajs.com/resources/integrations/index.html.md) or [How-to & Tutorials](https://docs.medusajs.com/resources/how-to-tutorials/index.html.md) sections. It's built with [Next.js 15](https://nextjs.org/). - - `api-reference`: includes the codebase for the API reference website. It's built with [Next.js 15](https://nextjs.org/). - - `ui`: includes the codebase for the Medusa UI documentation website. It's built with [Next.js 15](https://nextjs.org/). -- `packages`: this directory holds the shared packages and components necessary for the development of the projects in the `apps` directory. - - `docs-ui` includes the shared React components between the different apps. - - `remark-rehype-plugins` includes Remark and Rehype plugins used by the documentation projects. - -*** - -## Documentation Content - -All documentation projects are built with Next.js. The content is writtin in MDX files. - -### Medusa Main Docs Content - -The content of the Medusa main docs are under the `www/apps/book/app` directory. - -### Medusa Resources Content - -The content of all pages under the `/resources` path are under the `www/apps/resources/app` directory. - -Documentation pages under the `www/apps/resources/references` directory are generated automatically from the source code under the `packages/medusa` directory. So, you can't directly make changes to them. Instead, you'll have to make changes to the comments in the original source code. - -### API Reference - -The API reference's content is split into two types: - -1. Static content, which are the content related to getting started, expanding fields, and more. These are located in the `www/apps/api-reference/markdown` directory. They are MDX files. -2. OpenAPI specs that are shown to developers when checking the reference of an API Route. These are generated from OpenApi Spec comments, which are under the `www/utils/generated/oas-output` directory. - -### Medusa UI Documentation - -The content of the Medusa UI documentation are located under the `www/apps/ui/src/content/docs` directory. They are MDX files. - -The UI documentation also shows code examples, which are under the `www/apps/ui/src/examples` directory. - -The UI component props are generated from the source code and placed into the `www/apps/ui/src/specs` directory. To contribute to these props and their comments, check the comments in the source code under the `packages/design-system/ui` directory. - -*** - -## Style Guide - -When you contribute to the documentation content, make sure to follow the [documentation style guide](https://www.notion.so/Style-Guide-Docs-fad86dd1c5f84b48b145e959f36628e0). - -*** - -## How to Contribute - -If you’re fixing errors in an existing documentation page, you can scroll down to the end of the page and click on the “Edit this page” link. You’ll be redirected to the GitHub edit form of that page and you can make edits directly and submit a pull request (PR). - -If you’re adding a new page or contributing to the codebase, fork the repository, create a new branch, and make all changes necessary in your repository. Then, once you’re done, create a PR in the Medusa repository. - -### Base Branch - -When you make an edit to an existing documentation page or fork the repository to make changes to the documentation, create a new branch. - -Documentation contributions always use `develop` as the base branch. Make sure to also open your PR against the `develop` branch. - -### Branch Name - -Make sure that the branch name starts with `docs/`. For example, `docs/fix-services`. Vercel deployed previews are only triggered for branches starting with `docs/`. - -### Pull Request Conventions - -When you create a pull request, prefix the title with `docs:` or `docs(PROJECT_NAME):`, where `PROJECT_NAME` is the name of the documentation project this pull request pertains to. For example, `docs(ui): fix titles`. - -In the body of the PR, explain clearly what the PR does. If the PR solves an issue, use [closing keywords](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) with the issue number. For example, “Closes #1333”. - -*** - -## Images - -If you are adding images to a documentation page, you can host the image on [Imgur](https://imgur.com) for free to include it in the PR. Our team will later upload it to our image hosting. - -*** - -## NPM and Yarn Code Blocks - -If you’re adding code blocks that use NPM and Yarn, you must add the `npm2yarn` meta field. +In the configuration object, you pass a `timeout` property, whose value is a number indicating the timeout in seconds. For example: -````md -```bash npm2yarn -npm run start +```ts title="src/workflows/hello-world.ts" highlights={[["16"]]} collapsibleLines="1-13" expandButtonLabel="Show More" +import { + createStep, + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + "step-1", + async () => { + // ... + } +) + +const myWorkflow = createWorkflow({ + name: "hello-world", + timeout: 2, // 2 seconds +}, function () { + const str1 = step1() + + return new WorkflowResponse({ + message: str1, + }) +}) + +export default myWorkflow + ``` -```` -The code snippet must be written using NPM. +This workflow's executions fail if they run longer than two seconds. -### 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 -``` +A workflow’s timeout error is returned in the `errors` property of the workflow’s execution, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/errors/index.html.md). The error’s name is `TransactionTimeoutError`. *** -## Linting with Vale +## Configure Step Timeout -Medusa uses [Vale](https://vale.sh/) to lint documentation pages and perform checks on incoming PRs into the repository. +Alternatively, you can configure the timeout for a step rather than the entire workflow. -### Result of Vale PR Checks +As mentioned in the previous section, the timeout doesn't stop the execution of the step. It only affects the step's status and output. -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. +The step’s configuration object accepts a `timeout` property, whose value is a number indicating the timeout in seconds. -### Run Vale Locally +For example: -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 +```tsx +const step1 = createStep( + { + name: "step-1", + timeout: 2, // 2 seconds + }, + async () => { + // ... + } +) ``` -3\. Running the `run-vale` script: +This step's executions fail if they run longer than two seconds. -```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") -``` -~~~ */} +A step’s timeout error is returned in the `errors` property of the workflow’s execution, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/errors/index.html.md). The error’s name is `TransactionStepTimeoutError`. # Translate Medusa Admin @@ -18613,6 +18395,270 @@ const response = await api.post(`/custom`, form, { ``` +# Docs Contribution Guidelines + +Thank you for your interest in contributing to the documentation! You will be helping the open source community and other developers interested in learning more about Medusa and using it. + +This guide is specific to contributing to the documentation. If you’re interested in contributing to Medusa’s codebase, check out the [contributing guidelines in the Medusa GitHub repository](https://github.com/medusajs/medusa/blob/develop/CONTRIBUTING.md). + +## What Can You Contribute? + +You can contribute to the Medusa documentation in the following ways: + +- Fixes to existing content. This includes small fixes like typos, or adding missing information. +- Additions to the documentation. If you think a documentation page can be useful to other developers, you can contribute by adding it. + - Make sure to open an issue first in the [medusa repository](https://github.com/medusajs/medusa) to confirm that you can add that documentation page. +- Fixes to UI components and tooling. If you find a bug while browsing the documentation, you can contribute by fixing it. + +*** + +## Documentation Workspace + +Medusa's documentation projects are all part of the documentation yarn workspace, which you can find in the [medusa repository](https://github.com/medusajs/medusa) under the `www` directory. + +The workspace has the following two directories: + +- `apps`: this directory holds the different documentation websites and projects. + - `book`: includes the codebase for the [main Medusa documentation](https://docs.medusajs.com//index.html.md). It's built with [Next.js 15](https://nextjs.org/). + - `resources`: includes the codebase for the resources documentation, which powers different sections of the docs such as the [Integrations](https://docs.medusajs.com/resources/integrations/index.html.md) or [How-to & Tutorials](https://docs.medusajs.com/resources/how-to-tutorials/index.html.md) sections. It's built with [Next.js 15](https://nextjs.org/). + - `api-reference`: includes the codebase for the API reference website. It's built with [Next.js 15](https://nextjs.org/). + - `ui`: includes the codebase for the Medusa UI documentation website. It's built with [Next.js 15](https://nextjs.org/). +- `packages`: this directory holds the shared packages and components necessary for the development of the projects in the `apps` directory. + - `docs-ui` includes the shared React components between the different apps. + - `remark-rehype-plugins` includes Remark and Rehype plugins used by the documentation projects. + +*** + +## Documentation Content + +All documentation projects are built with Next.js. The content is writtin in MDX files. + +### Medusa Main Docs Content + +The content of the Medusa main docs are under the `www/apps/book/app` directory. + +### Medusa Resources Content + +The content of all pages under the `/resources` path are under the `www/apps/resources/app` directory. + +Documentation pages under the `www/apps/resources/references` directory are generated automatically from the source code under the `packages/medusa` directory. So, you can't directly make changes to them. Instead, you'll have to make changes to the comments in the original source code. + +### API Reference + +The API reference's content is split into two types: + +1. Static content, which are the content related to getting started, expanding fields, and more. These are located in the `www/apps/api-reference/markdown` directory. They are MDX files. +2. OpenAPI specs that are shown to developers when checking the reference of an API Route. These are generated from OpenApi Spec comments, which are under the `www/utils/generated/oas-output` directory. + +### Medusa UI Documentation + +The content of the Medusa UI documentation are located under the `www/apps/ui/src/content/docs` directory. They are MDX files. + +The UI documentation also shows code examples, which are under the `www/apps/ui/src/examples` directory. + +The UI component props are generated from the source code and placed into the `www/apps/ui/src/specs` directory. To contribute to these props and their comments, check the comments in the source code under the `packages/design-system/ui` directory. + +*** + +## Style Guide + +When you contribute to the documentation content, make sure to follow the [documentation style guide](https://www.notion.so/Style-Guide-Docs-fad86dd1c5f84b48b145e959f36628e0). + +*** + +## How to Contribute + +If you’re fixing errors in an existing documentation page, you can scroll down to the end of the page and click on the “Edit this page” link. You’ll be redirected to the GitHub edit form of that page and you can make edits directly and submit a pull request (PR). + +If you’re adding a new page or contributing to the codebase, fork the repository, create a new branch, and make all changes necessary in your repository. Then, once you’re done, create a PR in the Medusa repository. + +### Base Branch + +When you make an edit to an existing documentation page or fork the repository to make changes to the documentation, create a new branch. + +Documentation contributions always use `develop` as the base branch. Make sure to also open your PR against the `develop` branch. + +### Branch Name + +Make sure that the branch name starts with `docs/`. For example, `docs/fix-services`. Vercel deployed previews are only triggered for branches starting with `docs/`. + +### Pull Request Conventions + +When you create a pull request, prefix the title with `docs:` or `docs(PROJECT_NAME):`, where `PROJECT_NAME` is the name of the documentation project this pull request pertains to. For example, `docs(ui): fix titles`. + +In the body of the PR, explain clearly what the PR does. If the PR solves an issue, use [closing keywords](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) with the issue number. For example, “Closes #1333”. + +*** + +## Images + +If you are adding images to a documentation page, you can host the image on [Imgur](https://imgur.com) for free to include it in the PR. Our team will later upload it to our image hosting. + +*** + +## NPM and Yarn Code Blocks + +If you’re adding code blocks that use NPM and Yarn, you must add the `npm2yarn` meta field. + +For example: + +````md +```bash npm2yarn +npm run start +``` +```` + +The code snippet must be written using NPM. + +### Global Option + +When a command uses the global option `-g`, add it at the end of the NPM command to ensure that it’s transformed to a Yarn command properly. For example: + +```bash npm2yarn +npm install @medusajs/cli -g +``` + +*** + +## Linting with Vale + +Medusa uses [Vale](https://vale.sh/) to lint documentation pages and perform checks on incoming PRs into the repository. + +### Result of Vale PR Checks + +You can check the result of running the "lint" action on your PR by clicking the Details link next to it. You can find there all errors that you need to fix. + +### Run Vale Locally + +If you want to check your work locally, you can do that by: + +1. [Installing Vale](https://vale.sh/docs/vale-cli/installation/) on your machine. +2. Changing to the `www/vale` directory: + +```bash +cd www/vale +``` + +3\. Running the `run-vale` script: + +```bash +# to lint content for the main documentation +./run-vale.sh book/app/learn error resources +# to lint content for the resources documentation +./run-vale.sh resources/app error +# to lint content for the API reference +./run-vale.sh api-reference/markdown error +# to lint content for the Medusa UI documentation +./run-vale.sh ui/src/content/docs error +# to lint content for the user guide +./run-vale.sh user-guide/app error +``` + +{/* TODO need to enable MDX v1 comments first. */} + +{/* ### Linter Exceptions + +If it's needed to break some style guide rules in a document, you can wrap the parts that the linter shouldn't scan with the following comments in the `md` or `mdx` files: + +```md + + +content that shouldn't be scanned for errors here... + + +``` + +You can also disable specific rules. For example: + +```md + + +Medusa supports Node versions 14 and 16. + + +``` + +If you use this in your PR, you must justify its usage. */} + +*** + +## Linting with ESLint + +Medusa uses ESlint to lint code blocks both in the content and the code base of the documentation apps. + +### Linting Content with ESLint + +Each PR runs through a check that lints the code in the content files using ESLint. The action's name is `content-eslint`. + +If you want to check content ESLint errors locally and fix them, you can do that by: + +1\. Install the dependencies in the `www` directory: + +```bash +yarn install +``` + +2\. Run the turbo command in the `www` directory: + +```bash +turbo run lint:content +``` + +This will fix any fixable errors, and show errors that require your action. + +### Linting Code with ESLint + +Each PR runs through a check that lints the code in the content files using ESLint. The action's name is `code-docs-eslint`. + +If you want to check code ESLint errors locally and fix them, you can do that by: + +1\. Install the dependencies in the `www` directory: + +```bash +yarn install +``` + +2\. Run the turbo command in the `www` directory: + +```bash +yarn lint +``` + +This will fix any fixable errors, and show errors that require your action. + +{/* TODO need to enable MDX v1 comments first. */} + +{/* ### ESLint Exceptions + +If some code blocks have errors that can't or shouldn't be fixed, you can add the following command before the code block: + +~~~md + + +```js +console.log("This block isn't linted") +``` + +```js +console.log("This block is linted") +``` +~~~ + +You can also disable specific rules. For example: + +~~~md + + +```js +console.log("This block can use semicolons"); +``` + +```js +console.log("This block can't use semi colons") +``` +~~~ */} + + # Example: Write Integration Tests for Workflows In this chapter, you'll learn how to write integration tests for workflows using [medusaIntegrationTestRunner](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/integration-tests/index.html.md) from Medusa's Testing Framwork. @@ -18994,6 +19040,154 @@ 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. @@ -19135,173 +19329,6 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# 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. - -Refer to the Medusa Admin User Guide to learn how to use the dashboard to: - -- [Manage order fulfillments](https://docs.medusajs.com/user-guide/orders/fulfillments/index.html.md). -- [Manage shipping options and profiles](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/index.html.md). - -Medusa has fulfillment related features available out-of-the-box through the Fulfillment Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Fulfillment Module. - -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). - -## Fulfillment Features - -- [Fulfillment Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/item-fulfillment/index.html.md): Create fulfillments and keep track of their status, items, and more. -- [Integrate Third-Party Fulfillment Providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/fulfillment-provider/index.html.md): Create third-party fulfillment providers to provide customers with shipping options and fulfill their orders. -- [Restrict By Location and Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/shipping-option/index.html.md): Shipping options can be restricted to specific geographical locations. You can also specify custom rules to restrict shipping options. -- [Support Different Fulfillment Forms](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/concepts/index.html.md): Support various fulfillment forms, such as shipping or pick up. -- [Tiered Pricing and Price Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-rules/index.html.md): Set prices for shipping options with tiers and rules, allowing you to create complex pricing strategies. - -*** - -## How to Use the Fulfillment Module - -In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. - -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-fulfillment.ts" highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" - -const createFulfillmentStep = createStep( - "create-fulfillment", - async ({}, { container }) => { - const fulfillmentModuleService = container.resolve(Modules.FULFILLMENT) - - const fulfillment = await fulfillmentModuleService.createFulfillment({ - location_id: "loc_123", - provider_id: "webshipper", - delivery_address: { - country_code: "us", - city: "Strongsville", - address_1: "18290 Royalton Rd", - }, - items: [ - { - title: "Shirt", - sku: "SHIRT", - quantity: 1, - barcode: "123456", - }, - ], - labels: [], - order: {}, - }) - - return new StepResponse({ fulfillment }, fulfillment.id) - }, - async (fulfillmentId, { container }) => { - if (!fulfillmentId) { - return - } - const fulfillmentModuleService = container.resolve(Modules.FULFILLMENT) - - await fulfillmentModuleService.deleteFulfillment(fulfillmentId) - } -) - -export const createFulfillmentWorkflow = createWorkflow( - "create-fulfillment", - () => { - const { fulfillment } = createFulfillmentStep() - - return new WorkflowResponse({ - fulfillment, - }) - } -) -``` - -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 { createFulfillmentWorkflow } from "../../workflows/create-fuilfillment" - -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await createFulfillmentWorkflow(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 { createFulfillmentWorkflow } from "../workflows/create-fuilfillment" - -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await createFulfillmentWorkflow(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 { createFulfillmentWorkflow } from "../workflows/create-fuilfillment" - -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await createFulfillmentWorkflow(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 Fulfillment Module - -The Fulfillment Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/module-options/index.html.md) for details on the module's options. - -*** - - # 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. @@ -19582,24 +19609,30 @@ Medusa provides the following authentication providers out-of-the-box. You can u *** -# Currency Module +# Fulfillment 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. +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. -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. +Refer to the Medusa Admin User Guide to learn how to use the dashboard to: -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. +- [Manage order fulfillments](https://docs.medusajs.com/user-guide/orders/fulfillments/index.html.md). +- [Manage shipping options and profiles](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/index.html.md). + +Medusa has fulfillment related features available out-of-the-box through the Fulfillment Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Fulfillment Module. Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -## Currency Features +## Fulfillment 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. +- [Fulfillment Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/item-fulfillment/index.html.md): Create fulfillments and keep track of their status, items, and more. +- [Integrate Third-Party Fulfillment Providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/fulfillment-provider/index.html.md): Create third-party fulfillment providers to provide customers with shipping options and fulfill their orders. +- [Restrict By Location and Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/shipping-option/index.html.md): Shipping options can be restricted to specific geographical locations. You can also specify custom rules to restrict shipping options. +- [Support Different Fulfillment Forms](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/concepts/index.html.md): Support various fulfillment forms, such as shipping or pick up. +- [Tiered Pricing and Price Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-rules/index.html.md): Set prices for shipping options with tiers and rules, allowing you to create complex pricing strategies. *** -## How to Use the Currency Module +## How to Use the Fulfillment Module In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. @@ -19607,46 +19640,59 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/retrieve-price-with-currency.ts" highlights={highlights} +```ts title="src/workflows/create-fulfillment.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", +const createFulfillmentStep = createStep( + "create-fulfillment", async ({}, { container }) => { - const currencyModuleService = container.resolve(Modules.CURRENCY) + const fulfillmentModuleService = container.resolve(Modules.FULFILLMENT) - const currency = await currencyModuleService - .retrieveCurrency("usd") + const fulfillment = await fulfillmentModuleService.createFulfillment({ + location_id: "loc_123", + provider_id: "webshipper", + delivery_address: { + country_code: "us", + city: "Strongsville", + address_1: "18290 Royalton Rd", + }, + items: [ + { + title: "Shirt", + sku: "SHIRT", + quantity: 1, + barcode: "123456", + }, + ], + labels: [], + order: {}, + }) - return new StepResponse({ currency }) + return new StepResponse({ fulfillment }, fulfillment.id) + }, + async (fulfillmentId, { container }) => { + if (!fulfillmentId) { + return + } + const fulfillmentModuleService = container.resolve(Modules.FULFILLMENT) + + await fulfillmentModuleService.deleteFulfillment(fulfillmentId) } ) -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}` - }) +export const createFulfillmentWorkflow = createWorkflow( + "create-fulfillment", + () => { + const { fulfillment } = createFulfillmentStep() return new WorkflowResponse({ - formattedPrice, + fulfillment, }) } ) @@ -19656,21 +19702,19 @@ 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"], ["13"], ["14"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { retrievePriceWithCurrency } from "../../workflows/retrieve-price-with-currency" +import { createFulfillmentWorkflow } from "../../workflows/create-fuilfillment" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await retrievePriceWithCurrency(req.scope) - .run({ - price: 10, - }) + const { result } = await createFulfillmentWorkflow(req.scope) + .run() res.send(result) } @@ -19678,21 +19722,19 @@ export async function GET( ### Subscriber -```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"], ["13"], ["14"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +```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 { retrievePriceWithCurrency } from "../workflows/retrieve-price-with-currency" +import { createFulfillmentWorkflow } from "../workflows/create-fuilfillment" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await retrievePriceWithCurrency(container) - .run({ - price: 10, - }) + const { result } = await createFulfillmentWorkflow(container) + .run() console.log(result) } @@ -19704,17 +19746,15 @@ export const config: SubscriberConfig = { ### Scheduled Job -```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"], ["9"], ["10"]]} +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} import { MedusaContainer } from "@medusajs/framework/types" -import { retrievePriceWithCurrency } from "../workflows/retrieve-price-with-currency" +import { createFulfillmentWorkflow } from "../workflows/create-fuilfillment" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await retrievePriceWithCurrency(container) - .run({ - price: 10, - }) + const { result } = await createFulfillmentWorkflow(container) + .run() console.log(result) } @@ -19729,6 +19769,12 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** +## Configure Fulfillment Module + +The Fulfillment Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/module-options/index.html.md) for details on the module's options. + +*** + # Inventory Module @@ -19874,6 +19920,316 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** +# Order Module + +In this section of the documentation, you will find resources to learn more about the Order Module and how to use it in your application. + +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/index.html.md) to learn how to manage orders using the dashboard. + +Medusa has order related features available out-of-the-box through the Order Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Order Module. + +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). + +## Order Features + +- [Order Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/concepts/index.html.md): Store and manage your orders to retrieve, create, cancel, and perform other operations. +- Draft Orders: Allow merchants to create orders on behalf of their customers as draft orders that later are transformed to regular orders. +- [Apply Promotion Adjustments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/promotion-adjustments/index.html.md): Apply promotions or discounts to the order's items and shipping methods by adding adjustment lines that are factored into their subtotals. +- [Apply Tax Lines](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/tax-lines/index.html.md): Apply tax lines to an order's line items and shipping methods. +- [Returns](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md), [Edits](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/edit/index.html.md), [Exchanges](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/exchange/index.html.md), and [Claims](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/claim/index.html.md): Make [changes](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/order-change/index.html.md) to an order to edit, return, or exchange its items, with [version-based control](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/order-versioning/index.html.md) over the order's timeline. + +*** + +## How to Use the Order Module + +In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. + +You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. + +For example: + +```ts title="src/workflows/create-draft-order.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" + +const createDraftOrderStep = createStep( + "create-order", + async ({}, { container }) => { + const orderModuleService = container.resolve(Modules.ORDER) + + const draftOrder = await orderModuleService.createOrders({ + currency_code: "usd", + items: [ + { + title: "Shirt", + quantity: 1, + unit_price: 3000, + }, + ], + shipping_methods: [ + { + name: "Express shipping", + amount: 3000, + }, + ], + status: "draft", + }) + + return new StepResponse({ draftOrder }, draftOrder.id) + }, + async (draftOrderId, { container }) => { + if (!draftOrderId) { + return + } + const orderModuleService = container.resolve(Modules.ORDER) + + await orderModuleService.deleteOrders([draftOrderId]) + } +) + +export const createDraftOrderWorkflow = createWorkflow( + "create-draft-order", + () => { + const { draftOrder } = createDraftOrderStep() + + return new WorkflowResponse({ + draftOrder, + }) + } +) +``` + +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: + +### API Route + +```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { createDraftOrderWorkflow } from "../../workflows/create-draft-order" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createDraftOrderWorkflow(req.scope) + .run() + + res.send(result) +} +``` + +### Subscriber + +```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import { + type SubscriberConfig, + type SubscriberArgs, +} from "@medusajs/framework" +import { createDraftOrderWorkflow } from "../workflows/create-draft-order" + +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createDraftOrderWorkflow(container) + .run() + + console.log(result) +} + +export const config: SubscriberConfig = { + event: "user.created", +} +``` + +### Scheduled Job + +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { createDraftOrderWorkflow } from "../workflows/create-draft-order" + +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createDraftOrderWorkflow(container) + .run() + + console.log(result) +} + +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, +} +``` + +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). + +*** + + +# Pricing Module + +In this section of the documentation, you will find resources to learn more about the Pricing Module and how to use it in your application. + +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/price-lists/index.html.md) to learn how to manage price lists using the dashboard. + +Medusa has pricing related features available out-of-the-box through the Pricing Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Pricing Module. + +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). + +## Pricing Features + +- [Price Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/concepts/index.html.md): Store and manage prices of a resource, such as a product or a variant. +- [Advanced Rule Engine](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-rules/index.html.md): Create prices with tiers and custom rules to condition prices based on different contexts. +- [Price Lists](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/concepts#price-list/index.html.md): Group prices and apply them only in specific conditions with price lists. +- [Price Calculation Strategy](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md): Retrieve the best price in a given context and for the specified rule values. +- [Tax-Inclusive Pricing](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/tax-inclusive-pricing/index.html.md): Calculate prices with taxes included in the price, and Medusa will handle calculating the taxes automatically. + +*** + +## How to Use the Pricing Module + +In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. + +You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. + +For example: + +```ts title="src/workflows/create-price-set.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" + +const createPriceSetStep = createStep( + "create-price-set", + async ({}, { container }) => { + const pricingModuleService = container.resolve(Modules.PRICING) + + const priceSet = await pricingModuleService.createPriceSets({ + prices: [ + { + amount: 500, + currency_code: "USD", + }, + { + amount: 400, + currency_code: "EUR", + min_quantity: 0, + max_quantity: 4, + rules: {}, + }, + ], + }) + + return new StepResponse({ priceSet }, priceSet.id) + }, + async (priceSetId, { container }) => { + if (!priceSetId) { + return + } + const pricingModuleService = container.resolve(Modules.PRICING) + + await pricingModuleService.deletePriceSets([priceSetId]) + } +) + +export const createPriceSetWorkflow = createWorkflow( + "create-price-set", + () => { + const { priceSet } = createPriceSetStep() + + return new WorkflowResponse({ + priceSet, + }) + } +) +``` + +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: + +### API Route + +```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { createPriceSetWorkflow } from "../../workflows/create-price-set" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createPriceSetWorkflow(req.scope) + .run() + + res.send(result) +} +``` + +### Subscriber + +```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import { + type SubscriberConfig, + type SubscriberArgs, +} from "@medusajs/framework" +import { createPriceSetWorkflow } from "../workflows/create-price-set" + +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createPriceSetWorkflow(container) + .run() + + console.log(result) +} + +export const config: SubscriberConfig = { + event: "user.created", +} +``` + +### Scheduled Job + +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { createPriceSetWorkflow } from "../workflows/create-price-set" + +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createPriceSetWorkflow(container) + .run() + + console.log(result) +} + +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, +} +``` + +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). + +*** + + # Payment Module In this section of the documentation, you will find resources to learn more about the Payment Module and how to use it in your application. @@ -20029,27 +20385,26 @@ Medusa provides the following payment providers out-of-the-box. You can use them *** -# Order Module +# Promotion Module -In this section of the documentation, you will find resources to learn more about the Order Module and how to use it in your application. +In this section of the documentation, you will find resources to learn more about the Promotion Module and how to use it in your application. -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/index.html.md) to learn how to manage orders using the dashboard. +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/promotions/index.html.md) to learn how to manage promotions using the dashboard. -Medusa has order related features available out-of-the-box through the Order Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Order Module. +Medusa has promotion related features available out-of-the-box through the Promotion Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Promotion Module. Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -## Order Features +## Promotion Features -- [Order Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/concepts/index.html.md): Store and manage your orders to retrieve, create, cancel, and perform other operations. -- Draft Orders: Allow merchants to create orders on behalf of their customers as draft orders that later are transformed to regular orders. -- [Apply Promotion Adjustments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/promotion-adjustments/index.html.md): Apply promotions or discounts to the order's items and shipping methods by adding adjustment lines that are factored into their subtotals. -- [Apply Tax Lines](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/tax-lines/index.html.md): Apply tax lines to an order's line items and shipping methods. -- [Returns](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md), [Edits](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/edit/index.html.md), [Exchanges](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/exchange/index.html.md), and [Claims](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/claim/index.html.md): Make [changes](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/order-change/index.html.md) to an order to edit, return, or exchange its items, with [version-based control](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/order-versioning/index.html.md) over the order's timeline. +- [Discount Functionalities](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/concepts/index.html.md): A promotion discounts an amount or percentage of a cart's items, shipping methods, or the entire order. +- [Flexible Promotion Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/concepts#flexible-rules/index.html.md): A promotion has rules that restricts when the promotion is applied. +- [Campaign Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/campaign/index.html.md): A campaign combines promotions under the same conditions, such as start and end dates, and budget configurations. +- [Apply Promotion on Carts and Orders](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/actions/index.html.md): Apply promotions on carts and orders to discount items, shipping methods, or the entire order. *** -## How to Use the Order Module +## How to Use the Promotion Module In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. @@ -20057,7 +20412,7 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-draft-order.ts" highlights={highlights} +```ts title="src/workflows/create-promotion.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -20066,48 +20421,41 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createDraftOrderStep = createStep( - "create-order", +const createPromotionStep = createStep( + "create-promotion", async ({}, { container }) => { - const orderModuleService = container.resolve(Modules.ORDER) + const promotionModuleService = container.resolve(Modules.PROMOTION) - const draftOrder = await orderModuleService.createOrders({ - currency_code: "usd", - items: [ - { - title: "Shirt", - quantity: 1, - unit_price: 3000, - }, - ], - shipping_methods: [ - { - name: "Express shipping", - amount: 3000, - }, - ], - status: "draft", + const promotion = await promotionModuleService.createPromotions({ + code: "10%OFF", + type: "standard", + application_method: { + type: "percentage", + target_type: "order", + value: 10, + currency_code: "usd", + }, }) - return new StepResponse({ draftOrder }, draftOrder.id) + return new StepResponse({ promotion }, promotion.id) }, - async (draftOrderId, { container }) => { - if (!draftOrderId) { + async (promotionId, { container }) => { + if (!promotionId) { return } - const orderModuleService = container.resolve(Modules.ORDER) + const promotionModuleService = container.resolve(Modules.PROMOTION) - await orderModuleService.deleteOrders([draftOrderId]) + await promotionModuleService.deletePromotions(promotionId) } ) -export const createDraftOrderWorkflow = createWorkflow( - "create-draft-order", +export const createPromotionWorkflow = createWorkflow( + "create-promotion", () => { - const { draftOrder } = createDraftOrderStep() + const { promotion } = createPromotionStep() return new WorkflowResponse({ - draftOrder, + promotion, }) } ) @@ -20122,13 +20470,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createDraftOrderWorkflow } from "../../workflows/create-draft-order" +import { createPromotionWorkflow } from "../../workflows/create-cart" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createDraftOrderWorkflow(req.scope) + const { result } = await createPromotionWorkflow(req.scope) .run() res.send(result) @@ -20142,13 +20490,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createDraftOrderWorkflow } from "../workflows/create-draft-order" +import { createPromotionWorkflow } from "../workflows/create-cart" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createDraftOrderWorkflow(container) + const { result } = await createPromotionWorkflow(container) .run() console.log(result) @@ -20163,12 +20511,12 @@ export const config: SubscriberConfig = { ```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} import { MedusaContainer } from "@medusajs/framework/types" -import { createDraftOrderWorkflow } from "../workflows/create-draft-order" +import { createPromotionWorkflow } from "../workflows/create-cart" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createDraftOrderWorkflow(container) + const { result } = await createPromotionWorkflow(container) .run() console.log(result) @@ -20483,154 +20831,6 @@ 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. @@ -20791,160 +20991,6 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Pricing Module - -In this section of the documentation, you will find resources to learn more about the Pricing Module and how to use it in your application. - -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/price-lists/index.html.md) to learn how to manage price lists using the dashboard. - -Medusa has pricing related features available out-of-the-box through the Pricing Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Pricing Module. - -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). - -## Pricing Features - -- [Price Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/concepts/index.html.md): Store and manage prices of a resource, such as a product or a variant. -- [Advanced Rule Engine](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-rules/index.html.md): Create prices with tiers and custom rules to condition prices based on different contexts. -- [Price Lists](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/concepts#price-list/index.html.md): Group prices and apply them only in specific conditions with price lists. -- [Price Calculation Strategy](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md): Retrieve the best price in a given context and for the specified rule values. -- [Tax-Inclusive Pricing](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/tax-inclusive-pricing/index.html.md): Calculate prices with taxes included in the price, and Medusa will handle calculating the taxes automatically. - -*** - -## How to Use the Pricing Module - -In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. - -You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. - -For example: - -```ts title="src/workflows/create-price-set.ts" highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" - -const createPriceSetStep = createStep( - "create-price-set", - async ({}, { container }) => { - const pricingModuleService = container.resolve(Modules.PRICING) - - const priceSet = await pricingModuleService.createPriceSets({ - prices: [ - { - amount: 500, - currency_code: "USD", - }, - { - amount: 400, - currency_code: "EUR", - min_quantity: 0, - max_quantity: 4, - rules: {}, - }, - ], - }) - - return new StepResponse({ priceSet }, priceSet.id) - }, - async (priceSetId, { container }) => { - if (!priceSetId) { - return - } - const pricingModuleService = container.resolve(Modules.PRICING) - - await pricingModuleService.deletePriceSets([priceSetId]) - } -) - -export const createPriceSetWorkflow = createWorkflow( - "create-price-set", - () => { - const { priceSet } = createPriceSetStep() - - return new WorkflowResponse({ - priceSet, - }) - } -) -``` - -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: - -### API Route - -```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { createPriceSetWorkflow } from "../../workflows/create-price-set" - -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await createPriceSetWorkflow(req.scope) - .run() - - res.send(result) -} -``` - -### Subscriber - -```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import { - type SubscriberConfig, - type SubscriberArgs, -} from "@medusajs/framework" -import { createPriceSetWorkflow } from "../workflows/create-price-set" - -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await createPriceSetWorkflow(container) - .run() - - console.log(result) -} - -export const config: SubscriberConfig = { - event: "user.created", -} -``` - -### Scheduled Job - -```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} -import { MedusaContainer } from "@medusajs/framework/types" -import { createPriceSetWorkflow } from "../workflows/create-price-set" - -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await createPriceSetWorkflow(container) - .run() - - console.log(result) -} - -export const config = { - name: "run-once-a-day", - schedule: `0 0 * * *`, -} -``` - -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). - -*** - - # 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. @@ -21082,6 +21128,153 @@ 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. @@ -21368,153 +21561,6 @@ 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. - -*** - - # Links between API Key Module and Other Modules This document showcases the module links defined between the API Key Module and other Commerce Modules. @@ -21641,6 +21687,86 @@ The associated token is no longer usable or verifiable. 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. + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/customers/index.html.md) to learn how to manage customers using the dashboard. + +## `has_account` Property + +The [Customer data model](https://docs.medusajs.com/references/customer/models/Customer/index.html.md) has a `has_account` property, which is a boolean that indicates whether a customer is registered. + +When a guest customer places an order, a new `Customer` record is created with `has_account` set to `false`. + +When this or another guest customer registers an account with the same email, a new `Customer` record is created with `has_account` set to `true`. + +*** + +## Email Uniqueness + +The above behavior means that two `Customer` records may exist with the same email. However, the main difference is the `has_account` property's value. + +So, there can only be one guest customer (having `has_account=false`) and one registered customer (having `has_account=true`) with the same email. + + # Links between Customer Module and Other Modules This document showcases the module links defined between the Customer Module and other Commerce Modules. @@ -21818,679 +21944,6 @@ const { data: orders } = useQueryGraphStep({ ``` -# Customer Accounts - -In this document, you’ll learn how registered and unregistered accounts are distinguished in the Medusa application. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/customers/index.html.md) to learn how to manage customers using the dashboard. - -## `has_account` Property - -The [Customer data model](https://docs.medusajs.com/references/customer/models/Customer/index.html.md) has a `has_account` property, which is a boolean that indicates whether a customer is registered. - -When a guest customer places an order, a new `Customer` record is created with `has_account` set to `false`. - -When this or another guest customer registers an account with the same email, a new `Customer` record is created with `has_account` set to `true`. - -*** - -## Email Uniqueness - -The above behavior means that two `Customer` records may exist with the same email. However, the main difference is the `has_account` property's value. - -So, there can only be one guest customer (having `has_account=false`) and one registered customer (having `has_account=true`) with the same email. - - -# Fulfillment Concepts - -In this document, you’ll learn about some basic fulfillment concepts. - -## Fulfillment Set - -A fulfillment set is a general form or way of fulfillment. For example, shipping is a form of fulfillment, and pick-up is another form of fulfillment. Each of these can be created as fulfillment sets. - -A fulfillment set is represented by the [FulfillmentSet data model](https://docs.medusajs.com/references/fulfillment/models/FulfillmentSet/index.html.md). All other configurations, options, and management features are related to a fulfillment set, in one way or another. - -```ts -const fulfillmentSets = await fulfillmentModuleService.createFulfillmentSets( - [ - { - name: "Shipping", - type: "shipping", - }, - { - name: "Pick-up", - type: "pick-up", - }, - ] -) -``` - -*** - -## Service Zone - -A service zone is a collection of geographical zones or areas. It’s used to restrict available shipping options to a defined set of locations. - -A service zone is represented by the [ServiceZone data model](https://docs.medusajs.com/references/fulfillment/models/ServiceZone/index.html.md). It’s associated with a fulfillment set, as each service zone is specific to a form of fulfillment. For example, if a customer chooses to pick up items, you can restrict the available shipping options based on their location. - -![A diagram showcasing the relation between fulfillment sets, service zones, and geo zones](https://res.cloudinary.com/dza7lstvk/image/upload/v1712329770/Medusa%20Resources/service-zone_awmvfs.jpg) - -A service zone can have multiple geographical zones, each represented by the [GeoZone data model](https://docs.medusajs.com/references/fulfillment/models/GeoZone/index.html.md). It holds location-related details to narrow down supported areas, such as country, city, or province code. - -The province code is always in lower-case and in [ISO 3166-2 format](https://en.wikipedia.org/wiki/ISO_3166-2). - -*** - -## Shipping Profile - -A shipping profile defines a type of items that are shipped in a similar manner. For example, a `default` shipping profile is used for all item types, but the `digital` shipping profile is used for digital items that aren’t shipped and delivered conventionally. - -A shipping profile is represented by the [ShippingProfile data model](https://docs.medusajs.com/references/fulfillment/models/ShippingProfile/index.html.md). It only defines the profile’s details, but it’s associated with the shipping options available for the item type. - - -# Fulfillment Module Provider - -In this guide, you’ll learn about the Fulfillment Module Provider and how it's used. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/locations#manage-fulfillment-providers/index.html.md) to learn how to add a fulfillment provider to a location using the dashboard. - -## What is a Fulfillment Module Provider? - -A Fulfillment Module Provider handles fulfilling items, typically using a third-party integration. - -Fulfillment Module Providers registered in the Fulfillment Module's [options](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/module-options/index.html.md) are stored and represented by the [FulfillmentProvider data model](https://docs.medusajs.com/references/fulfillment/models/FulfillmentProvider/index.html.md). - -![Diagram showcasing the communication between Medusa, the Fulfillment Module Provider, and the third-party fulfillment provider.](https://res.cloudinary.com/dza7lstvk/image/upload/v1746794800/Medusa%20Resources/fulfillment-provider-service_ljsqpq.jpg) - -*** - -## Default Fulfillment Provider - -Medusa provides a Manual Fulfillment Provider that acts as a placeholder fulfillment provider. It doesn't process fulfillment and delegates that to the merchant. - -This provider is installed by default in your application and you can use it to fulfill items manually. - -The identifier of the manual fulfillment provider is `fp_manual_manual`. - -*** - -## How to Create a Custom Fulfillment Provider? - -A Fulfillment Module Provider is a module whose service implements the `IFulfillmentProvider` imported from `@medusajs/framework/types`. - -The module can have multiple fulfillment provider services, where each are registered as separate fulfillment providers. - -Refer to the [Create Fulfillment Module Provider](https://docs.medusajs.com/references/fulfillment/provider/index.html.md) guide to learn how to create a Fulfillment Module Provider. - -{/* TODO add link to user guide */} - -After you create a fulfillment provider, you can choose it as the default Fulfillment Module Provider for a stock location in the Medusa Admin dashboard. - -*** - -## How are Fulfillment Providers Registered? - -### Configure Fulfillment Module's Providers - -The Fulfillment Module accepts a `providers` option that allows you to configure the providers registered in your application. - -Learn more about this option in the [Module Options](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/module-options/index.html.md) guide. - -### Registration on Application Start - -When the Medusa application starts, it registers the Fulfillment Module Providers defined in the `providers` option of the Fulfillment Module. - -For each Fulfillment Module Provider, the Medusa application finds all fulfillment provider services defined in them to register. - -### FulfillmentProvider Data Model - -A registered fulfillment provider is represented by the [FulfillmentProvider data model](https://docs.medusajs.com/references/fulfillment/models/FulfillmentProvider/index.html.md) in the Medusa application. - -This data model is used to reference a service in the Fulfillment Module Provider and determine whether it's installed in the application. - -![Diagram showcasing the FulfillmentProvider data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1746794803/Medusa%20Resources/fulfillment-provider-model_wo2ato.jpg) - -The `FulfillmentProvider` data model has the following properties: - -- `id`: The unique identifier of the fulfillment provider. The ID's format is `fp_{identifier}_{id}`, where: - - `identifier` is the value of the `identifier` property in the Fulfillment Module Provider's service. - - `id` is the value of the `id` property of the Fulfillment Module Provider in `medusa-config.ts`. -- `is_enabled`: A boolean indicating whether the fulfillment provider is enabled. - -### How to Remove a Fulfillment Provider? - -You can remove a registered fulfillment provider from the Medusa application by removing it from the `providers` option in the Fulfillment Module's configuration. - -Then, the next time the Medusa application starts, it will set the `is_enabled` property of the `FulfillmentProvider`'s record to `false`. This allows you to re-enable the fulfillment provider later if needed by adding it back to the `providers` option. - - -# Links between Fulfillment Module and Other Modules - -This document showcases the module links defined between the Fulfillment Module and other Commerce Modules. - -## Summary - -The Fulfillment Module has the following links to other modules: - -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -|Order|Fulfillment|Stored - one-to-many|Learn more| -|Return|Fulfillment|Stored - one-to-many|Learn more| -|PriceSet|ShippingOption|Stored - many-to-one|Learn more| -|Product|ShippingProfile|Stored - many-to-one|Learn more| -|StockLocation|FulfillmentProvider|Stored - one-to-many|Learn more| -|StockLocation|FulfillmentSet|Stored - one-to-many|Learn more| - -*** - -## Order Module - -The [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md) provides order-management functionalities. - -Medusa defines a link between the `Fulfillment` and `Order` data models. A fulfillment is created for an orders' items. - -![A diagram showcasing an example of how data models from the Fulfillment and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716549903/Medusa%20Resources/order-fulfillment_h0vlps.jpg) - -A fulfillment is also created for a return's items. So, Medusa defines a link between the `Fulfillment` and `Return` data models. - -![A diagram showcasing an example of how data models from the Fulfillment and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728399052/Medusa%20Resources/Social_Media_Graphics_2024_Order_Return_vetimk.jpg) - -### Retrieve with Query - -To retrieve the order of a fulfillment with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `order.*` in `fields`: - -To retrieve the return, pass `return.*` in `fields`. - -### query.graph - -```ts -const { data: fulfillments } = await query.graph({ - entity: "fulfillment", - fields: [ - "order.*", - ], -}) - -// fulfillments.order -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: fulfillments } = useQueryGraphStep({ - entity: "fulfillment", - fields: [ - "order.*", - ], -}) - -// fulfillments.order -``` - -### Manage with Link - -To manage the order of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.ORDER]: { - order_id: "order_123", - }, - [Modules.FULFILLMENT]: { - fulfillment_id: "ful_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.ORDER]: { - order_id: "order_123", - }, - [Modules.FULFILLMENT]: { - fulfillment_id: "ful_123", - }, -}) -``` - -*** - -## Pricing Module - -The Pricing Module provides features to store, manage, and retrieve the best prices in a specified context. - -Medusa defines a link between the `PriceSet` and `ShippingOption` data models. A shipping option's price is stored as a price set. - -![A diagram showcasing an example of how data models from the Pricing and Fulfillment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716561747/Medusa%20Resources/pricing-fulfillment_spywwa.jpg) - -### Retrieve with Query - -To retrieve the price set of a shipping option with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `price_set.*` in `fields`: - -### query.graph - -```ts -const { data: shippingOptions } = await query.graph({ - entity: "shipping_option", - fields: [ - "price_set_link.*", - ], -}) - -// shippingOptions[0].price_set_link?.price_set_id -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: shippingOptions } = useQueryGraphStep({ - entity: "shipping_option", - fields: [ - "price_set_link.*", - ], -}) - -// shippingOptions[0].price_set_link?.price_set_id -``` - -### Manage with Link - -To manage the price set of a shipping option, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.FULFILLMENT]: { - shipping_option_id: "so_123", - }, - [Modules.PRICING]: { - price_set_id: "pset_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.FULFILLMENT]: { - shipping_option_id: "so_123", - }, - [Modules.PRICING]: { - price_set_id: "pset_123", - }, -}) -``` - -*** - -## Product Module - -Medusa defines a link between the `ShippingProfile` data model and the `Product` data model of the Product Module. Each product must belong to a shipping profile. - -This link is introduced in [Medusa v2.5.0](https://github.com/medusajs/medusa/releases/tag/v2.5.0). - -### Retrieve with Query - -To retrieve the products of a shipping profile with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `products.*` in `fields`: - -### query.graph - -```ts -const { data: shippingProfiles } = await query.graph({ - entity: "shipping_profile", - fields: [ - "products.*", - ], -}) - -// shippingProfiles[0].products -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: shippingProfiles } = useQueryGraphStep({ - entity: "shipping_profile", - fields: [ - "products.*", - ], -}) - -// shippingProfiles[0].products -``` - -### Manage with Link - -To manage the shipping profile of a product, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - [Modules.FULFILLMENT]: { - shipping_profile_id: "sp_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - [Modules.FULFILLMENT]: { - shipping_profile_id: "sp_123", - }, -}) -``` - -*** - -## Stock Location Module - -The Stock Location Module provides features to manage stock locations in a store. - -Medusa defines a link between the `FulfillmentSet` and `StockLocation` data models. A fulfillment set can be conditioned to a specific stock location. - -![A diagram showcasing an example of how data models from the Fulfillment and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1712567101/Medusa%20Resources/fulfillment-stock-location_nlkf7e.jpg) - -Medusa also defines a link between the `FulfillmentProvider` and `StockLocation` data models to indicate the providers that can be used in a location. - -![A diagram showcasing an example of how data models from the Fulfillment and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728399492/Medusa%20Resources/fulfillment-provider-stock-location_b0mulo.jpg) - -### Retrieve with Query - -To retrieve the stock location of a fulfillment set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `location.*` in `fields`: - -To retrieve the stock location of a fulfillment provider, pass `locations.*` in `fields`. - -### query.graph - -```ts -const { data: fulfillmentSets } = await query.graph({ - entity: "fulfillment_set", - fields: [ - "location.*", - ], -}) - -// fulfillmentSets[0].location -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: fulfillmentSets } = useQueryGraphStep({ - entity: "fulfillment_set", - fields: [ - "location.*", - ], -}) - -// fulfillmentSets[0].location -``` - -### Manage with Link - -To manage the stock location of a fulfillment set, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.STOCK_LOCATION]: { - stock_location_id: "sloc_123", - }, - [Modules.FULFILLMENT]: { - fulfillment_set_id: "fset_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.STOCK_LOCATION]: { - stock_location_id: "sloc_123", - }, - [Modules.FULFILLMENT]: { - fulfillment_set_id: "fset_123", - }, -}) -``` - - -# Item Fulfillment - -In this document, you’ll learn about the concepts of item fulfillment. - -## Fulfillment Data Model - -A fulfillment is the shipping and delivery of one or more items to the customer. It’s represented by the [Fulfillment data model](https://docs.medusajs.com/references/fulfillment/models/Fulfillment/index.html.md). - -*** - -## Fulfillment Processing by a Fulfillment Provider - -A fulfillment is associated with a fulfillment provider that handles all its processing, such as creating a shipment for the fulfillment’s items. - -The fulfillment is also associated with a shipping option of that provider, which determines how the item is shipped. - -![A diagram showcasing the relation between a fulfillment, fulfillment provider, and shipping option](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331947/Medusa%20Resources/fulfillment-shipping-option_jk9ndp.jpg) - -*** - -## data Property - -The `Fulfillment` data model has a `data` property that holds any necessary data for the third-party fulfillment provider to process the fulfillment. - -For example, the `data` property can hold the ID of the fulfillment in the third-party provider. The associated fulfillment provider then uses it whenever it retrieves the fulfillment’s details. - -*** - -## Fulfillment Items - -A fulfillment is used to fulfill one or more items. Each item is represented by the `FulfillmentItem` data model. - -The fulfillment item holds details relevant to fulfilling the item, such as barcode, SKU, and quantity to fulfill. - -![A diagram showcasing the relation between fulfillment and fulfillment items.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712332114/Medusa%20Resources/fulfillment-item_etzxb0.jpg) - -*** - -## Fulfillment Label - -Once a shipment is created for the fulfillment, you can store its tracking number, URL, or other related details as a label, represented by the `FulfillmentLabel` data model. - -*** - -## Fulfillment Status - -The `Fulfillment` data model has three properties to keep track of the current status of the fulfillment: - -- `packed_at`: The date the fulfillment was packed. If set, then the fulfillment has been packed. -- `shipped_at`: The date the fulfillment was shipped. If set, then the fulfillment has been shipped. -- `delivered_at`: The date the fulfillment was delivered. If set, then the fulfillment has been delivered. - - -# Fulfillment Module Options - -In this document, you'll learn about the options of the Fulfillment Module. - -## providers - -The `providers` option is an array of fulfillment module providers. - -When the Medusa application starts, these providers are registered and can be used to process fulfillments. - -For example: - -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" - -// ... - -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/fulfillment", - options: { - providers: [ - { - resolve: `@medusajs/medusa/fulfillment-manual`, - id: "manual", - options: { - // provider options... - }, - }, - ], - }, - }, - ], -}) -``` - -The `providers` option is an array of objects that accept the following properties: - -- `resolve`: A string indicating either the package name of the module provider or the path to it relative to the `src` directory. -- `id`: A string indicating the provider's unique name or ID. -- `options`: An optional object of the module provider's options. - - -# Shipping Option - -In this document, you’ll learn about shipping options and their rules. - -## What’s a Shipping Option? - -A shipping option is a way of shipping an item. Each fulfillment provider provides a set of shipping options. For example, a provider may provide a shipping option for express shipping and another for standard shipping. - -When the customer places their order, they choose a shipping option to be used to fulfill their items. - -A shipping option is represented by the [ShippingOption data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOption/index.html.md). - -*** - -## Service Zone Restrictions - -A shipping option is restricted by a service zone, limiting the locations a shipping option be used in. - -For example, a fulfillment provider may have a shipping option that can be used in the United States, and another in Canada. - -![A diagram showcasing the relation between shipping options and service zones.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712330831/Medusa%20Resources/shipping-option-service-zone_pobh6k.jpg) - -Service zones can be more restrictive, such as restricting to certain cities or province codes. - -The province code is always in lower-case and in [ISO 3166-2 format](https://en.wikipedia.org/wiki/ISO_3166-2). - -![A diagram showcasing the relation between shipping options, service zones, and geo zones](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331186/Medusa%20Resources/shipping-option-service-zone-city_m5sxod.jpg) - -*** - -## Shipping Option Rules - -You can restrict shipping options by custom rules, such as the item’s weight or the customer’s group. - -You can also restrict a shipping option's price based on specific conditions. For example, you can make a shipping option's price free based on the cart's total. Learn more in the Pricing Module's [Price Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-rules#how-to-set-rules-on-a-price/index.html.md) guide. - -These rules are represented by the [ShippingOptionRule data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOptionRule/index.html.md). Its properties define the custom rule: - -- `attribute`: The name of a property or table that the rule applies to. For example, `customer_group`. -- `operator`: The operator used in the condition. For example: - - To allow multiple values, use the operator `in`, which validates that the provided values are in the rule’s values. - - To create a negation condition that considers `value` against the rule, use `nin`, which validates that the provided values aren’t in the rule’s values. - - Check out more operators in [this reference](https://docs.medusajs.com/references/fulfillment/types/fulfillment.RuleOperatorType/index.html.md). -- `value`: One or more values. - -![A diagram showcasing the relation between shipping option and shipping option rules.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331340/Medusa%20Resources/shipping-option-rule_oosopf.jpg) - -A shipping option can have multiple rules. For example, you can add rules to a shipping option so that it's available if the customer belongs to the VIP group and the total weight is less than 2000g. - -![A diagram showcasing how a shipping option can have multiple rules.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331462/Medusa%20Resources/shipping-option-rule-2_ylaqdb.jpg) - -*** - -## Shipping Profile and Types - -A shipping option belongs to a type. For example, a shipping option’s type may be `express`, while another `standard`. The type is represented by the [ShippingOptionType data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOptionType/index.html.md). - -A shipping option also belongs to a shipping profile, as each shipping profile defines the type of items to be shipped in a similar manner. - -*** - -## data Property - -When fulfilling an item, you might use a third-party fulfillment provider that requires additional custom data to be passed along from the checkout or order-creation process. - -The `ShippingOption` data model has a `data` property. It's an object that stores custom data relevant later when creating and processing a fulfillment. - - # Cart Concepts In this document, you’ll get an overview of the main concepts of a cart. @@ -23431,58 +22884,6 @@ For example, if you have a custom module with a `Manager` data model, you can au 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. @@ -23827,70 +23228,56 @@ If the authentication is successful, the request returns an object with a `succe ``` -# Auth Module Options +# Auth Module Provider -In this document, you'll learn about the options of the Auth Module. +In this guide, you’ll learn about the Auth Module Provider and how it's used. -## providers +## What is an Auth Module Provider? -The `providers` option is an array of auth module providers. +An Auth Module Provider handles authenticating customers and users, either using custom logic or by integrating a third-party service. -When the Medusa application starts, these providers are registered and can be used to handle authentication. +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. -By default, the `emailpass` provider is registered to authenticate customers and admin users. +### Auth Providers List -For example: +- [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" -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... - }, - }, - ], + projectConfig: { + http: { + authMethodsPerActor: { + user: ["google"], + customer: ["emailpass"], }, + // ... }, - ], + // ... + }, }) ``` -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). +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 @@ -24292,6 +23679,72 @@ In the workflow, you: 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). @@ -24406,43 +23859,231 @@ The page shows the user password fields to enter their new password, then submit - [Storefront Guide: Reset Customer Password](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/reset-password/index.html.md) -# Links between Currency Module and Other Modules +# Fulfillment Concepts -This document showcases the module links defined between the Currency Module and other Commerce Modules. +In this document, you’ll learn about some basic fulfillment concepts. -## Summary +## Fulfillment Set -The Currency Module has the following links to other modules: +A fulfillment set is a general form or way of fulfillment. For example, shipping is a form of fulfillment, and pick-up is another form of fulfillment. Each of these can be created as fulfillment sets. -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. +A fulfillment set is represented by the [FulfillmentSet data model](https://docs.medusajs.com/references/fulfillment/models/FulfillmentSet/index.html.md). All other configurations, options, and management features are related to a fulfillment set, in one way or another. -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -|StoreCurrency|Currency|Read-only - has one|Learn more| +```ts +const fulfillmentSets = await fulfillmentModuleService.createFulfillmentSets( + [ + { + name: "Shipping", + type: "shipping", + }, + { + name: "Pick-up", + type: "pick-up", + }, + ] +) +``` *** -## Store Module +## Service Zone -The Store Module has a `Currency` data model that stores the supported currencies of a store. However, these currencies don't hold all the details of a currency, such as its name or symbol. +A service zone is a collection of geographical zones or areas. It’s used to restrict available shipping options to a defined set of locations. -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. +A service zone is represented by the [ServiceZone data model](https://docs.medusajs.com/references/fulfillment/models/ServiceZone/index.html.md). It’s associated with a fulfillment set, as each service zone is specific to a form of fulfillment. For example, if a customer chooses to pick up items, you can restrict the available shipping options based on their location. + +![A diagram showcasing the relation between fulfillment sets, service zones, and geo zones](https://res.cloudinary.com/dza7lstvk/image/upload/v1712329770/Medusa%20Resources/service-zone_awmvfs.jpg) + +A service zone can have multiple geographical zones, each represented by the [GeoZone data model](https://docs.medusajs.com/references/fulfillment/models/GeoZone/index.html.md). It holds location-related details to narrow down supported areas, such as country, city, or province code. + +The province code is always in lower-case and in [ISO 3166-2 format](https://en.wikipedia.org/wiki/ISO_3166-2). + +*** + +## Shipping Profile + +A shipping profile defines a type of items that are shipped in a similar manner. For example, a `default` shipping profile is used for all item types, but the `digital` shipping profile is used for digital items that aren’t shipped and delivered conventionally. + +A shipping profile is represented by the [ShippingProfile data model](https://docs.medusajs.com/references/fulfillment/models/ShippingProfile/index.html.md). It only defines the profile’s details, but it’s associated with the shipping options available for the item type. + + +# Fulfillment Module Provider + +In this guide, you’ll learn about the Fulfillment Module Provider and how it's used. + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/locations#manage-fulfillment-providers/index.html.md) to learn how to add a fulfillment provider to a location using the dashboard. + +## What is a Fulfillment Module Provider? + +A Fulfillment Module Provider handles fulfilling items, typically using a third-party integration. + +Fulfillment Module Providers registered in the Fulfillment Module's [options](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/module-options/index.html.md) are stored and represented by the [FulfillmentProvider data model](https://docs.medusajs.com/references/fulfillment/models/FulfillmentProvider/index.html.md). + +![Diagram showcasing the communication between Medusa, the Fulfillment Module Provider, and the third-party fulfillment provider.](https://res.cloudinary.com/dza7lstvk/image/upload/v1746794800/Medusa%20Resources/fulfillment-provider-service_ljsqpq.jpg) + +*** + +## Default Fulfillment Provider + +Medusa provides a Manual Fulfillment Provider that acts as a placeholder fulfillment provider. It doesn't process fulfillment and delegates that to the merchant. + +This provider is installed by default in your application and you can use it to fulfill items manually. + +The identifier of the manual fulfillment provider is `fp_manual_manual`. + +*** + +## How to Create a Custom Fulfillment Provider? + +A Fulfillment Module Provider is a module whose service implements the `IFulfillmentProvider` imported from `@medusajs/framework/types`. + +The module can have multiple fulfillment provider services, where each are registered as separate fulfillment providers. + +Refer to the [Create Fulfillment Module Provider](https://docs.medusajs.com/references/fulfillment/provider/index.html.md) guide to learn how to create a Fulfillment Module Provider. + +{/* TODO add link to user guide */} + +After you create a fulfillment provider, you can choose it as the default Fulfillment Module Provider for a stock location in the Medusa Admin dashboard. + +*** + +## How are Fulfillment Providers Registered? + +### Configure Fulfillment Module's Providers + +The Fulfillment Module accepts a `providers` option that allows you to configure the providers registered in your application. + +Learn more about this option in the [Module Options](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/module-options/index.html.md) guide. + +### Registration on Application Start + +When the Medusa application starts, it registers the Fulfillment Module Providers defined in the `providers` option of the Fulfillment Module. + +For each Fulfillment Module Provider, the Medusa application finds all fulfillment provider services defined in them to register. + +### FulfillmentProvider Data Model + +A registered fulfillment provider is represented by the [FulfillmentProvider data model](https://docs.medusajs.com/references/fulfillment/models/FulfillmentProvider/index.html.md) in the Medusa application. + +This data model is used to reference a service in the Fulfillment Module Provider and determine whether it's installed in the application. + +![Diagram showcasing the FulfillmentProvider data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1746794803/Medusa%20Resources/fulfillment-provider-model_wo2ato.jpg) + +The `FulfillmentProvider` data model has the following properties: + +- `id`: The unique identifier of the fulfillment provider. The ID's format is `fp_{identifier}_{id}`, where: + - `identifier` is the value of the `identifier` property in the Fulfillment Module Provider's service. + - `id` is the value of the `id` property of the Fulfillment Module Provider in `medusa-config.ts`. +- `is_enabled`: A boolean indicating whether the fulfillment provider is enabled. + +### How to Remove a Fulfillment Provider? + +You can remove a registered fulfillment provider from the Medusa application by removing it from the `providers` option in the Fulfillment Module's configuration. + +Then, the next time the Medusa application starts, it will set the `is_enabled` property of the `FulfillmentProvider`'s record to `false`. This allows you to re-enable the fulfillment provider later if needed by adding it back to the `providers` option. + + +# Item Fulfillment + +In this document, you’ll learn about the concepts of item fulfillment. + +## Fulfillment Data Model + +A fulfillment is the shipping and delivery of one or more items to the customer. It’s represented by the [Fulfillment data model](https://docs.medusajs.com/references/fulfillment/models/Fulfillment/index.html.md). + +*** + +## Fulfillment Processing by a Fulfillment Provider + +A fulfillment is associated with a fulfillment provider that handles all its processing, such as creating a shipment for the fulfillment’s items. + +The fulfillment is also associated with a shipping option of that provider, which determines how the item is shipped. + +![A diagram showcasing the relation between a fulfillment, fulfillment provider, and shipping option](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331947/Medusa%20Resources/fulfillment-shipping-option_jk9ndp.jpg) + +*** + +## data Property + +The `Fulfillment` data model has a `data` property that holds any necessary data for the third-party fulfillment provider to process the fulfillment. + +For example, the `data` property can hold the ID of the fulfillment in the third-party provider. The associated fulfillment provider then uses it whenever it retrieves the fulfillment’s details. + +*** + +## Fulfillment Items + +A fulfillment is used to fulfill one or more items. Each item is represented by the `FulfillmentItem` data model. + +The fulfillment item holds details relevant to fulfilling the item, such as barcode, SKU, and quantity to fulfill. + +![A diagram showcasing the relation between fulfillment and fulfillment items.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712332114/Medusa%20Resources/fulfillment-item_etzxb0.jpg) + +*** + +## Fulfillment Label + +Once a shipment is created for the fulfillment, you can store its tracking number, URL, or other related details as a label, represented by the `FulfillmentLabel` data model. + +*** + +## Fulfillment Status + +The `Fulfillment` data model has three properties to keep track of the current status of the fulfillment: + +- `packed_at`: The date the fulfillment was packed. If set, then the fulfillment has been packed. +- `shipped_at`: The date the fulfillment was shipped. If set, then the fulfillment has been shipped. +- `delivered_at`: The date the fulfillment was delivered. If set, then the fulfillment has been delivered. + + +# Links between Fulfillment Module and Other Modules + +This document showcases the module links defined between the Fulfillment Module and other Commerce Modules. + +## Summary + +The Fulfillment Module has the following links to other modules: + +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +|Order|Fulfillment|Stored - one-to-many|Learn more| +|Return|Fulfillment|Stored - one-to-many|Learn more| +|PriceSet|ShippingOption|Stored - many-to-one|Learn more| +|Product|ShippingProfile|Stored - many-to-one|Learn more| +|StockLocation|FulfillmentProvider|Stored - one-to-many|Learn more| +|StockLocation|FulfillmentSet|Stored - one-to-many|Learn more| + +*** + +## Order Module + +The [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md) provides order-management functionalities. + +Medusa defines a link between the `Fulfillment` and `Order` data models. A fulfillment is created for an orders' items. + +![A diagram showcasing an example of how data models from the Fulfillment and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716549903/Medusa%20Resources/order-fulfillment_h0vlps.jpg) + +A fulfillment is also created for a return's items. So, Medusa defines a link between the `Fulfillment` and `Return` data models. + +![A diagram showcasing an example of how data models from the Fulfillment and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728399052/Medusa%20Resources/Social_Media_Graphics_2024_Order_Return_vetimk.jpg) ### Retrieve with Query -To retrieve the 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 order of a fulfillment with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `order.*` in `fields`: + +To retrieve the return, pass `return.*` in `fields`. ### query.graph ```ts -const { data: stores } = await query.graph({ - entity: "store", +const { data: fulfillments } = await query.graph({ + entity: "fulfillment", fields: [ - "supported_currencies.currency.*", + "order.*", ], }) -// stores[0].supported_currencies[0].currency +// fulfillments.order ``` ### useQueryGraphStep @@ -24452,16 +24093,421 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: stores } = useQueryGraphStep({ - entity: "store", +const { data: fulfillments } = useQueryGraphStep({ + entity: "fulfillment", fields: [ - "supported_currencies.currency.*", + "order.*", ], }) -// stores[0].supported_currencies[0].currency +// fulfillments.order ``` +### Manage with Link + +To manage the order of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.ORDER]: { + order_id: "order_123", + }, + [Modules.FULFILLMENT]: { + fulfillment_id: "ful_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.ORDER]: { + order_id: "order_123", + }, + [Modules.FULFILLMENT]: { + fulfillment_id: "ful_123", + }, +}) +``` + +*** + +## Pricing Module + +The Pricing Module provides features to store, manage, and retrieve the best prices in a specified context. + +Medusa defines a link between the `PriceSet` and `ShippingOption` data models. A shipping option's price is stored as a price set. + +![A diagram showcasing an example of how data models from the Pricing and Fulfillment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716561747/Medusa%20Resources/pricing-fulfillment_spywwa.jpg) + +### Retrieve with Query + +To retrieve the price set of a shipping option with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `price_set.*` in `fields`: + +### query.graph + +```ts +const { data: shippingOptions } = await query.graph({ + entity: "shipping_option", + fields: [ + "price_set_link.*", + ], +}) + +// shippingOptions[0].price_set_link?.price_set_id +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: shippingOptions } = useQueryGraphStep({ + entity: "shipping_option", + fields: [ + "price_set_link.*", + ], +}) + +// shippingOptions[0].price_set_link?.price_set_id +``` + +### Manage with Link + +To manage the price set of a shipping option, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.FULFILLMENT]: { + shipping_option_id: "so_123", + }, + [Modules.PRICING]: { + price_set_id: "pset_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.FULFILLMENT]: { + shipping_option_id: "so_123", + }, + [Modules.PRICING]: { + price_set_id: "pset_123", + }, +}) +``` + +*** + +## Product Module + +Medusa defines a link between the `ShippingProfile` data model and the `Product` data model of the Product Module. Each product must belong to a shipping profile. + +This link is introduced in [Medusa v2.5.0](https://github.com/medusajs/medusa/releases/tag/v2.5.0). + +### Retrieve with Query + +To retrieve the products of a shipping profile with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `products.*` in `fields`: + +### query.graph + +```ts +const { data: shippingProfiles } = await query.graph({ + entity: "shipping_profile", + fields: [ + "products.*", + ], +}) + +// shippingProfiles[0].products +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: shippingProfiles } = useQueryGraphStep({ + entity: "shipping_profile", + fields: [ + "products.*", + ], +}) + +// shippingProfiles[0].products +``` + +### Manage with Link + +To manage the shipping profile of a product, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + [Modules.FULFILLMENT]: { + shipping_profile_id: "sp_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + [Modules.FULFILLMENT]: { + shipping_profile_id: "sp_123", + }, +}) +``` + +*** + +## Stock Location Module + +The Stock Location Module provides features to manage stock locations in a store. + +Medusa defines a link between the `FulfillmentSet` and `StockLocation` data models. A fulfillment set can be conditioned to a specific stock location. + +![A diagram showcasing an example of how data models from the Fulfillment and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1712567101/Medusa%20Resources/fulfillment-stock-location_nlkf7e.jpg) + +Medusa also defines a link between the `FulfillmentProvider` and `StockLocation` data models to indicate the providers that can be used in a location. + +![A diagram showcasing an example of how data models from the Fulfillment and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728399492/Medusa%20Resources/fulfillment-provider-stock-location_b0mulo.jpg) + +### Retrieve with Query + +To retrieve the stock location of a fulfillment set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `location.*` in `fields`: + +To retrieve the stock location of a fulfillment provider, pass `locations.*` in `fields`. + +### query.graph + +```ts +const { data: fulfillmentSets } = await query.graph({ + entity: "fulfillment_set", + fields: [ + "location.*", + ], +}) + +// fulfillmentSets[0].location +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: fulfillmentSets } = useQueryGraphStep({ + entity: "fulfillment_set", + fields: [ + "location.*", + ], +}) + +// fulfillmentSets[0].location +``` + +### Manage with Link + +To manage the stock location of a fulfillment set, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.STOCK_LOCATION]: { + stock_location_id: "sloc_123", + }, + [Modules.FULFILLMENT]: { + fulfillment_set_id: "fset_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.STOCK_LOCATION]: { + stock_location_id: "sloc_123", + }, + [Modules.FULFILLMENT]: { + fulfillment_set_id: "fset_123", + }, +}) +``` + + +# Fulfillment Module Options + +In this document, you'll learn about the options of the Fulfillment Module. + +## providers + +The `providers` option is an array of fulfillment module providers. + +When the Medusa application starts, these providers are registered and can be used to process fulfillments. + +For example: + +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" + +// ... + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/fulfillment", + options: { + providers: [ + { + resolve: `@medusajs/medusa/fulfillment-manual`, + id: "manual", + options: { + // provider options... + }, + }, + ], + }, + }, + ], +}) +``` + +The `providers` option is an array of objects that accept the following properties: + +- `resolve`: A string indicating either the package name of the module provider or the path to it relative to the `src` directory. +- `id`: A string indicating the provider's unique name or ID. +- `options`: An optional object of the module provider's options. + + +# Shipping Option + +In this document, you’ll learn about shipping options and their rules. + +## What’s a Shipping Option? + +A shipping option is a way of shipping an item. Each fulfillment provider provides a set of shipping options. For example, a provider may provide a shipping option for express shipping and another for standard shipping. + +When the customer places their order, they choose a shipping option to be used to fulfill their items. + +A shipping option is represented by the [ShippingOption data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOption/index.html.md). + +*** + +## Service Zone Restrictions + +A shipping option is restricted by a service zone, limiting the locations a shipping option be used in. + +For example, a fulfillment provider may have a shipping option that can be used in the United States, and another in Canada. + +![A diagram showcasing the relation between shipping options and service zones.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712330831/Medusa%20Resources/shipping-option-service-zone_pobh6k.jpg) + +Service zones can be more restrictive, such as restricting to certain cities or province codes. + +The province code is always in lower-case and in [ISO 3166-2 format](https://en.wikipedia.org/wiki/ISO_3166-2). + +![A diagram showcasing the relation between shipping options, service zones, and geo zones](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331186/Medusa%20Resources/shipping-option-service-zone-city_m5sxod.jpg) + +*** + +## Shipping Option Rules + +You can restrict shipping options by custom rules, such as the item’s weight or the customer’s group. + +You can also restrict a shipping option's price based on specific conditions. For example, you can make a shipping option's price free based on the cart's total. Learn more in the Pricing Module's [Price Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-rules#how-to-set-rules-on-a-price/index.html.md) guide. + +These rules are represented by the [ShippingOptionRule data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOptionRule/index.html.md). Its properties define the custom rule: + +- `attribute`: The name of a property or table that the rule applies to. For example, `customer_group`. +- `operator`: The operator used in the condition. For example: + - To allow multiple values, use the operator `in`, which validates that the provided values are in the rule’s values. + - To create a negation condition that considers `value` against the rule, use `nin`, which validates that the provided values aren’t in the rule’s values. + - Check out more operators in [this reference](https://docs.medusajs.com/references/fulfillment/types/fulfillment.RuleOperatorType/index.html.md). +- `value`: One or more values. + +![A diagram showcasing the relation between shipping option and shipping option rules.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331340/Medusa%20Resources/shipping-option-rule_oosopf.jpg) + +A shipping option can have multiple rules. For example, you can add rules to a shipping option so that it's available if the customer belongs to the VIP group and the total weight is less than 2000g. + +![A diagram showcasing how a shipping option can have multiple rules.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331462/Medusa%20Resources/shipping-option-rule-2_ylaqdb.jpg) + +*** + +## Shipping Profile and Types + +A shipping option belongs to a type. For example, a shipping option’s type may be `express`, while another `standard`. The type is represented by the [ShippingOptionType data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOptionType/index.html.md). + +A shipping option also belongs to a shipping profile, as each shipping profile defines the type of items to be shipped in a similar manner. + +*** + +## data Property + +When fulfilling an item, you might use a third-party fulfillment provider that requires additional custom data to be passed along from the checkout or order-creation process. + +The `ShippingOption` data model has a `data` property. It's an object that stores custom data relevant later when creating and processing a fulfillment. + # Inventory Concepts @@ -24506,6 +24552,67 @@ A reservation item, represented by the [ReservationItem](https://docs.medusajs.c The reserved quantity is associated with a location, so it has a similar relation to that of the `InventoryLevel` with the Stock Location Module. +# Inventory Module in Medusa Flows + +This document explains how the Inventory Module is used within the Medusa application's flows. + +## Product Variant Creation + +When a product variant is created and its `manage_inventory` property's value is `true`, the Medusa application creates an inventory item associated with that product variant. + +This flow is implemented within the [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/index.html.md) + +![A diagram showcasing how the Inventory Module is used in the product variant creation form](https://res.cloudinary.com/dza7lstvk/image/upload/v1709661511/Medusa%20Resources/inventory-product-create_khz2hk.jpg) + +*** + +## Add to Cart + +When a product variant with `manage_inventory` set to `true` is added to cart, the Medusa application checks whether there's sufficient stocked quantity. If not, an error is thrown and the product variant won't be added to the cart. + +This flow is implemented within the [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md) + +![A diagram showcasing how the Inventory Module is used in the add to cart flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709711645/Medusa%20Resources/inventory-cart-flow_achwq9.jpg) + +*** + +## Order Placed + +When an order is placed, the Medusa application creates a reservation item for each product variant with `manage_inventory` set to `true`. + +This flow is implemented within the [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) + +![A diagram showcasing how the Inventory Module is used in the order placed flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709712005/Medusa%20Resources/inventory-order-placed_qdxqdn.jpg) + +*** + +## Order Fulfillment + +When an item in an order is fulfilled and the associated variant has its `manage_inventory` property set to `true`, the Medusa application: + +- Subtracts the `reserved_quantity` from the `stocked_quantity` in the inventory level associated with the variant's inventory item. +- Resets the `reserved_quantity` to `0`. +- Deletes the associated reservation item. + +This flow is implemented within the [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md) + +![A diagram showcasing how the Inventory Module is used in the order fulfillment flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709712390/Medusa%20Resources/inventory-order-fulfillment_o9wdxh.jpg) + +*** + +## Order Return + +When an item in an order is returned and the associated variant has its `manage_inventory` property set to `true`, the Medusa application increments the `stocked_quantity` of the inventory item's level with the returned quantity. + +This flow is implemented within the [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/index.html.md) + +![A diagram showcasing how the Inventory Module is used in the order return flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709712457/Medusa%20Resources/inventory-order-return_ihftyk.jpg) + +### Dismissed Returned Items + +If a returned item is considered damaged or is dismissed, its quantity doesn't increment the `stocked_quantity` of the inventory item's level. + + # Inventory Kits In this guide, you'll learn how inventory kits can be used in the Medusa application to support use cases like multi-part products, bundled products, and shared inventory across products. @@ -24896,116 +25003,6 @@ The bundled product has the same inventory items as those of the products part o You can now [execute the workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows#3-execute-the-workflow/index.html.md) in [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md), [scheduled jobs](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md), or [subscribers](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md). -# Inventory Module in Medusa Flows - -This document explains how the Inventory Module is used within the Medusa application's flows. - -## Product Variant Creation - -When a product variant is created and its `manage_inventory` property's value is `true`, the Medusa application creates an inventory item associated with that product variant. - -This flow is implemented within the [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/index.html.md) - -![A diagram showcasing how the Inventory Module is used in the product variant creation form](https://res.cloudinary.com/dza7lstvk/image/upload/v1709661511/Medusa%20Resources/inventory-product-create_khz2hk.jpg) - -*** - -## Add to Cart - -When a product variant with `manage_inventory` set to `true` is added to cart, the Medusa application checks whether there's sufficient stocked quantity. If not, an error is thrown and the product variant won't be added to the cart. - -This flow is implemented within the [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md) - -![A diagram showcasing how the Inventory Module is used in the add to cart flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709711645/Medusa%20Resources/inventory-cart-flow_achwq9.jpg) - -*** - -## Order Placed - -When an order is placed, the Medusa application creates a reservation item for each product variant with `manage_inventory` set to `true`. - -This flow is implemented within the [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) - -![A diagram showcasing how the Inventory Module is used in the order placed flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709712005/Medusa%20Resources/inventory-order-placed_qdxqdn.jpg) - -*** - -## Order Fulfillment - -When an item in an order is fulfilled and the associated variant has its `manage_inventory` property set to `true`, the Medusa application: - -- Subtracts the `reserved_quantity` from the `stocked_quantity` in the inventory level associated with the variant's inventory item. -- Resets the `reserved_quantity` to `0`. -- Deletes the associated reservation item. - -This flow is implemented within the [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md) - -![A diagram showcasing how the Inventory Module is used in the order fulfillment flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709712390/Medusa%20Resources/inventory-order-fulfillment_o9wdxh.jpg) - -*** - -## Order Return - -When an item in an order is returned and the associated variant has its `manage_inventory` property set to `true`, the Medusa application increments the `stocked_quantity` of the inventory item's level with the returned quantity. - -This flow is implemented within the [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/index.html.md) - -![A diagram showcasing how the Inventory Module is used in the order return flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709712457/Medusa%20Resources/inventory-order-return_ihftyk.jpg) - -### Dismissed Returned Items - -If a returned item is considered damaged or is dismissed, its quantity doesn't increment the `stocked_quantity` of the inventory item's level. - - -# Account Holders and Saved Payment Methods - -In this documentation, you'll learn about account holders, and how they're used to save payment methods in third-party payment providers. - -Account holders are available starting from Medusa `v2.5.0`. - -## What's an Account Holder? - -An account holder represents a customer that can have saved payment methods in a third-party service. It's represented by the `AccountHolder` data model. - -It holds fields retrieved from the third-party provider, such as: - -- `external_id`: The ID of the equivalent customer or account holder in the third-party provider. -- `data`: Data returned by the payment provider when the account holder is created. - -A payment provider that supports saving payment methods for customers would create the equivalent of an account holder in the third-party provider. Then, whenever a payment method is saved, it would be saved under the account holder in the third-party provider. - -### Relation between Account Holder and Customer - -The Medusa application creates a link between the [Customer](https://docs.medusajs.com/references/customer/models/Customer/index.html.md) data model of the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md) and the `AccountHolder` data model of the Payment Module. - -This link indicates that a customer can have more than one account holder, each representing saved payment methods in different payment providers. - -Learn more about this link in the [Link to Other Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/links-to-other-modules/index.html.md) guide. - -*** - -## Save Payment Methods - -If a payment provider supports saving payment methods for a customer, they must implement the following methods: - -- `createAccountHolder`: Creates an account holder in the payment provider. The Payment Module uses this method before creating the account holder in Medusa, and uses the returned data to set fields like `external_id` and `data` in the created `AccountHolder` record. -- `deleteAccountHolder`: Deletes an account holder in the payment provider. The Payment Module uses this method when an account holder is deleted in Medusa. -- `savePaymentMethod`: Saves a payment method for an account holder in the payment provider. -- `listPaymentMethods`: Lists saved payment methods in the third-party service for an account holder. This is useful when displaying the customer's saved payment methods in the storefront. - -Learn more about implementing these methods in the [Create Payment Provider guide](https://docs.medusajs.com/references/payment/provider/index.html.md). - -*** - -## Account Holder in Medusa Payment Flows - -In the Medusa application, when a payment session is created for a registered customer, the Medusa application uses the Payment Module to create an account holder for the customer. - -Consequently, the Payment Module uses the payment provider to create an account holder in the third-party service, then creates the account holder in Medusa. - -This flow is only supported if the chosen payment provider has implemented the necessary [save payment methods](#save-payment-methods). - - # Links between Inventory Module and Other Modules This document showcases the module links defined between the Inventory Module and other Commerce Modules. @@ -25147,910 +25144,6 @@ const { data: inventoryLevels } = useQueryGraphStep({ ``` -# 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 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 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. - - -# 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 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 Webhook Events - -In this guide, you’ll learn how you can handle payment webhook events in your Medusa application and using the Payment Module. - -## What's a Payment Webhook Event? - -A payment webhook event is a request sent from a third-party payment provider to your application. It indicates a change in a payment’s status. - -This is useful in many cases such as: - -- When a payment is processed (authorized or captured) asynchronously. -- When a payment is managed on the third-party payment provider's side. -- When a payment action on the frontend was interrupted, leading the payment to be processed without an order being created in the Medusa application. - -So, it's essential to handle webhook events to ensure that your application is aware of updated payment statuses and can take appropriate actions. - -*** - -## How to Handle Payment Webhook Events - -### Webhook Listener API Route - -The Medusa application has a `/hooks/payment/[identifier]_[provider]` API route out-of-the-box that allows you to listen to webhook events from third-party payment providers, where: - -- `[identifier]` is the `identifier` static property defined in the payment provider. For example, `stripe`. -- `[provider]` is the ID of the provider. For example, `stripe`. - -For example, when integrating basic Stripe payments with the [Stripe Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md), the webhook listener route is `/hooks/payment/stripe_stripe`. - -You can use this webhook listener when configuring webhook events in your third-party payment provider. - -### getWebhookActionAndData Method - -The webhook listener API route executes the [getWebhookActionAndData method](https://docs.medusajs.com/references/payment/getWebhookActionAndData/index.html.md) of the Payment Module's main service. This method delegates handling of incoming webhook events to the relevant payment provider. - -Payment providers have a similar [getWebhookActionAndData method](https://docs.medusajs.com/references/payment/provider/index.html.md) to process the webhook event. So, if you're implementing a custom payment provider, make sure to implement it to handle webhook events. - -![A diagram showcasing the steps of how the getWebhookActionAndData method words](https://res.cloudinary.com/dza7lstvk/image/upload/v1711567415/Medusa%20Resources/payment-webhook_seaocg.jpg) - -If the `getWebhookActionAndData` method returns an `authorized` or `captured` action, the Medusa application will perform one of the following actions: - -View the full flow of the webhook event processing in the [processPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/processPaymentWorkflow/index.html.md) reference. - -- If the method returns an `authorized` action, Medusa will set the associated payment session to `authorized`. -- If the method returns a `captured` action, Medusa will set the associated payment session to `captured`. -- In either cases, if the cart associated with the payment session is not completed yet, Medusa will complete the cart. - - -# Payment 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. - - # Order Claim In this document, you’ll learn about order claims. @@ -26154,6 +25247,63 @@ An order can have multiple transactions. The sum of these transactions must be e Learn more about transactions in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/transactions/index.html.md). +# Order Edit + +In this document, you'll learn about order edits. + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/edit/index.html.md) to learn how to edit an order's items using the dashboard. + +## What is an Order Edit? + +A merchant can edit an order to add new items or change the quantity of existing items in the order. + +An order edit is represented by the [OrderChange data model](https://docs.medusajs.com/references/order/models/OrderChange/index.html.md). + +The `OrderChange` data model is associated with any type of change, including a return or exchange. However, its `change_type` property distinguishes the type of change it's making. + +In the case of an order edit, the `OrderChange`'s type is `edit`. + +*** + +## Add Items in an Order Edit + +When the merchant adds new items to the order in the order edit, the item is added as an [OrderItem](https://docs.medusajs.com/references/order/models/OrderItem/index.html.md). + +Also, an `OrderChangeAction` is created. The [OrderChangeAction data model](https://docs.medusajs.com/references/order/models/OrderChangeAction/index.html.md) represents a change made by an `OrderChange`, such as an item added. + +So, when an item is added, an `OrderChangeAction` is created with the type `ITEM_ADD`. In its `details` property, the item's ID, price, and quantity are stored. + +*** + +## Update Items in an Order Edit + +A merchant can update an existing item's quantity or price. + +This change is added as an `OrderChangeAction` with the type `ITEM_UPDATE`. In its `details` property, the item's ID, new price, and new quantity are stored. + +*** + +## Shipping Methods of New Items in the Edit + +Adding new items to the order requires adding shipping methods for those items. + +These shipping methods are represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderItem/index.html.md). Also, an `OrderChangeAction` is created with the type `SHIPPING_ADD` + +*** + +## How Order Edits Impact an Order’s Version + +When an order edit is confirmed, the order’s version is incremented. + +*** + +## Payments and Refunds for Order Edit Changes + +Once the Order Edit is confirmed, any additional payment or refund required can be made on the original order. + +This is determined by the comparison between the `OrderSummary` and the order's transactions, as mentioned in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/transactions#checking-outstanding-amount/index.html.md). + + # Order Exchange In this document, you’ll learn about order exchanges. @@ -26731,63 +25881,6 @@ const { data: orders } = useQueryGraphStep({ ``` -# Order Edit - -In this document, you'll learn about order edits. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/edit/index.html.md) to learn how to edit an order's items using the dashboard. - -## What is an Order Edit? - -A merchant can edit an order to add new items or change the quantity of existing items in the order. - -An order edit is represented by the [OrderChange data model](https://docs.medusajs.com/references/order/models/OrderChange/index.html.md). - -The `OrderChange` data model is associated with any type of change, including a return or exchange. However, its `change_type` property distinguishes the type of change it's making. - -In the case of an order edit, the `OrderChange`'s type is `edit`. - -*** - -## Add Items in an Order Edit - -When the merchant adds new items to the order in the order edit, the item is added as an [OrderItem](https://docs.medusajs.com/references/order/models/OrderItem/index.html.md). - -Also, an `OrderChangeAction` is created. The [OrderChangeAction data model](https://docs.medusajs.com/references/order/models/OrderChangeAction/index.html.md) represents a change made by an `OrderChange`, such as an item added. - -So, when an item is added, an `OrderChangeAction` is created with the type `ITEM_ADD`. In its `details` property, the item's ID, price, and quantity are stored. - -*** - -## Update Items in an Order Edit - -A merchant can update an existing item's quantity or price. - -This change is added as an `OrderChangeAction` with the type `ITEM_UPDATE`. In its `details` property, the item's ID, new price, and new quantity are stored. - -*** - -## Shipping Methods of New Items in the Edit - -Adding new items to the order requires adding shipping methods for those items. - -These shipping methods are represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderItem/index.html.md). Also, an `OrderChangeAction` is created with the type `SHIPPING_ADD` - -*** - -## How Order Edits Impact an Order’s Version - -When an order edit is confirmed, the order’s version is incremented. - -*** - -## Payments and Refunds for Order Edit Changes - -Once the Order Edit is confirmed, any additional payment or refund required can be made on the original order. - -This is determined by the comparison between the `OrderSummary` and the order's transactions, as mentioned in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/transactions#checking-outstanding-amount/index.html.md). - - # Order Versioning In this document, you’ll learn how an order and its details are versioned. @@ -26818,6 +25911,106 @@ 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. @@ -26940,104 +26133,33 @@ await orderModuleService.setOrderShippingMethodAdjustments( ``` -# Order Change +# Tax Lines in Order Module -In this document, you'll learn about the Order Change data model and possible actions in it. +In this document, you’ll learn about tax lines in an order. -## OrderChange Data Model +## What are Tax Lines? -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. +A tax line indicates the tax rate of a line item or a shipping method. -Its `change_type` property indicates what the order change is created for: +The [OrderLineItemTaxLine data model](https://docs.medusajs.com/references/order/models/OrderLineItemTaxLine/index.html.md) represents a line item’s tax line, and the [OrderShippingMethodTaxLine data model](https://docs.medusajs.com/references/order/models/OrderShippingMethodTaxLine/index.html.md) represents a shipping method’s tax line. -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. +![A diagram showcasing the relation between orders, items and shipping methods, and tax lines](https://res.cloudinary.com/dza7lstvk/image/upload/v1712307225/Medusa%20Resources/order-tax-lines_sixujd.jpg) *** -## Order Change Actions +## Tax Inclusivity -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). +By default, the tax amount is calculated by taking the tax rate from the line item or shipping method’s amount and then adding it to the item/method’s subtotal. -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. +However, line items and shipping methods have an `is_tax_inclusive` property that, when enabled, indicates that the item or method’s price already includes taxes. -The following table lists the possible `action` values that Medusa uses and what `details` they carry. +So, instead of calculating the tax rate and adding it to the item/method’s subtotal, it’s calculated as part of the subtotal. -|Action|Description|Details| -|---|---|---|---|---| -|\`ITEM\_ADD\`|Add an item to the order.|\`details\`| -|\`ITEM\_UPDATE\`|Update an item in the order.|\`details\`| -|\`RETURN\_ITEM\`|Set an item to be returned.|\`details\`| -|\`RECEIVE\_RETURN\_ITEM\`|Mark a return item as received.|\`details\`| -|\`RECEIVE\_DAMAGED\_RETURN\_ITEM\`|Mark a return item that's damaged as received.|\`details\`| -|\`SHIPPING\_ADD\`|Add a shipping method for new or returned items.|No details added. The ID to the shipping method is added in the | -|\`SHIPPING\_ADD\`|Add a shipping method for new or returned items.|No details added. The ID to the shipping method is added in the | -|\`WRITE\_OFF\_ITEM\`|Remove an item's quantity as part of the claim, without adding the quantity back to the item variant's inventory.|\`details\`| +The following diagram is a simplified showcase of how a subtotal is calculated from the tax perspective. +![A diagram showcasing how a subtotal is calculated from the tax perspective](https://res.cloudinary.com/dza7lstvk/image/upload/v1712307395/Medusa%20Resources/order-tax-inclusive_oebdnm.jpg) -# 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. +For example, if a line item's amount is `5000`, the tax rate is `10`, and `is_tax_inclusive` is enabled, the tax amount is 10% of `5000`, which is `500`. The item's unit price becomes `4500`. # Transactions @@ -27088,122 +26210,67 @@ 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`. -# Tax Lines in Order Module +# Pricing Concepts -In this document, you’ll learn about tax lines in an order. +In this document, you’ll learn about the main concepts in the Pricing Module. -## What are Tax Lines? +## Price Set -A tax line indicates the tax rate of a line item or a shipping method. +A [PriceSet](https://docs.medusajs.com/references/pricing/models/PriceSet/index.html.md) represents a collection of prices that are linked to a resource (for example, a product or a shipping option). -The [OrderLineItemTaxLine data model](https://docs.medusajs.com/references/order/models/OrderLineItemTaxLine/index.html.md) represents a line item’s tax line, and the [OrderShippingMethodTaxLine data model](https://docs.medusajs.com/references/order/models/OrderShippingMethodTaxLine/index.html.md) represents a shipping method’s tax line. +Each of these prices are represented by the [Price data module](https://docs.medusajs.com/references/pricing/models/Price/index.html.md). -![A diagram showcasing the relation between orders, items and shipping methods, and tax lines](https://res.cloudinary.com/dza7lstvk/image/upload/v1712307225/Medusa%20Resources/order-tax-lines_sixujd.jpg) +![A diagram showcasing the relation between the price set and price](https://res.cloudinary.com/dza7lstvk/image/upload/v1709648650/Medusa%20Resources/price-set-money-amount_xeees0.jpg) *** -## Tax Inclusivity +## Price List -By default, the tax amount is calculated by taking the tax rate from the line item or shipping method’s amount and then adding it to the item/method’s subtotal. +A [PriceList](https://docs.medusajs.com/references/pricing/models/PriceList/index.html.md) is a group of prices only enabled if their conditions and rules are satisfied. -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. +A price list has optional `start_date` and `end_date` properties that indicate the date range in which a price list can be applied. -So, instead of calculating the tax rate and adding it to the item/method’s subtotal, it’s calculated as part of the subtotal. - -The following diagram is a simplified showcase of how a subtotal is calculated from the tax perspective. - -![A diagram showcasing how a subtotal is calculated from the tax perspective](https://res.cloudinary.com/dza7lstvk/image/upload/v1712307395/Medusa%20Resources/order-tax-inclusive_oebdnm.jpg) - -For example, if a line item's amount is `5000`, the tax rate is `10`, and `is_tax_inclusive` is enabled, the tax amount is 10% of `5000`, which is `500`. The item's unit price becomes `4500`. +Its associated prices are represented by the `Price` data model. -# Links between Product Module and Other Modules +# Links between Pricing Module and Other Modules -This document showcases the module links defined between the Product Module and other Commerce Modules. +This document showcases the module links defined between the Pricing Module and other Commerce Modules. ## Summary -The Product Module has the following links to other modules: - -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. +The Pricing Module has the following links to other modules: |First Data Model|Second Data Model|Type|Description| |---|---|---|---| -|LineItem|Product|Read-only - has one|Learn more| -|Product|ShippingProfile|Stored - many-to-one|Learn more| -|ProductVariant|InventoryItem|Stored - many-to-many|Learn more| -|OrderLineItem|Product|Read-only - has one|Learn more| +|ShippingOption|PriceSet|Stored - one-to-one|Learn more| |ProductVariant|PriceSet|Stored - one-to-one|Learn more| -|Product|SalesChannel|Stored - many-to-many|Learn more| - -*** - -## Cart Module - -Medusa defines read-only links between: - -- The [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `LineItem` data model and the `Product` data model. Because the link is read-only from the `LineItem`'s side, you can only retrieve the product of a line item, and not the other way around. -- The `ProductVariant` data model and the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `LineItem` data model. Because the link is read-only from the `LineItem`'s side, you can only retrieve the variant of a line item, and not the other way around. - -### 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[0].variant -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: lineItems } = useQueryGraphStep({ - entity: "line_item", - fields: [ - "variant.*", - ], -}) - -// lineItems[0].variant -``` *** ## Fulfillment Module -Medusa defines a link between the `Product` data model and the `ShippingProfile` data model of the Fulfillment Module. Each product must belong to a shipping profile. +The Fulfillment Module provides fulfillment-related functionalities, including shipping options that the customer chooses from when they place their order. However, it doesn't provide pricing-related functionalities for these options. -This link is introduced in [Medusa v2.5.0](https://github.com/medusajs/medusa/releases/tag/v2.5.0). +Medusa defines a link between the `PriceSet` and `ShippingOption` data models. A shipping option's price is stored as a price set. + +![A diagram showcasing an example of how data models from the Pricing and Fulfillment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716561747/Medusa%20Resources/pricing-fulfillment_spywwa.jpg) ### Retrieve with Query -To retrieve the shipping profile of a product with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `shipping_profile.*` in `fields`: +To retrieve the shipping option of a price set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `shipping_option.*` in `fields`: ### query.graph ```ts -const { data: products } = await query.graph({ - entity: "product", +const { data: priceSets } = await query.graph({ + entity: "price_set", fields: [ - "shipping_profile.*", + "shipping_option.*", ], }) -// products[0].shipping_profile +// priceSets[0].shipping_option ``` ### useQueryGraphStep @@ -27213,19 +26280,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: products } = useQueryGraphStep({ - entity: "product", +const { data: priceSets } = useQueryGraphStep({ + entity: "price_set", fields: [ - "shipping_profile.*", + "shipping_option.*", ], }) -// products[0].shipping_profile +// priceSets[0].shipping_option ``` ### Manage with Link -To manage the shipping profile of a product, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +To manage the price set of a shipping option, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -27235,11 +26302,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, [Modules.FULFILLMENT]: { - shipping_profile_id: "sp_123", + shipping_option_id: "so_123", + }, + [Modules.PRICING]: { + price_set_id: "pset_123", }, }) ``` @@ -27253,128 +26320,44 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, [Modules.FULFILLMENT]: { - shipping_profile_id: "sp_123", + shipping_option_id: "so_123", + }, + [Modules.PRICING]: { + price_set_id: "pset_123", }, }) ``` *** -## Inventory Module +## Product Module -The Inventory Module provides inventory-management features for any stock-kept item. +The Product Module doesn't store or manage the prices of product variants. -Medusa defines a link between the `ProductVariant` and `InventoryItem` data models. Each product variant has different inventory details. +Medusa defines a link between the `ProductVariant` and the `PriceSet`. A product variant’s prices are stored as prices belonging to a price set. -![A diagram showcasing an example of how data models from the Product and Inventory modules are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709652779/Medusa%20Resources/product-inventory_kmjnud.jpg) +![A diagram showcasing an example of how data models from the Pricing and Product Module are linked. The PriceSet is linked to the ProductVariant of the Product Module.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709651039/Medusa%20Resources/pricing-product_m4xaut.jpg) -When the `manage_inventory` property of a product variant is enabled, you can manage the variant's inventory in different locations through this relation. +So, when you want to add prices for a product variant, you create a price set and add the prices to it. -Learn more about product variant's inventory management in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/variant-inventory/index.html.md). +You can then benefit from adding rules to prices or using the `calculatePrices` method to retrieve the price of a product variant within a specified context. ### Retrieve with Query -To retrieve the inventory items of a product variant with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `inventory_items.*` in `fields`: +To retrieve the variant of a price set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variant.*` in `fields`: ### query.graph ```ts -const { data: variants } = await query.graph({ - entity: "variant", - fields: [ - "inventory_items.*", - ], -}) - -// variants[0].inventory_items -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: variants } = useQueryGraphStep({ - entity: "variant", - fields: [ - "inventory_items.*", - ], -}) - -// variants[0].inventory_items -``` - -### Manage with Link - -To manage the inventory items of a variant, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.PRODUCT]: { - variant_id: "variant_123", - }, - [Modules.INVENTORY]: { - inventory_item_id: "iitem_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.PRODUCT]: { - variant_id: "variant_123", - }, - [Modules.INVENTORY]: { - inventory_item_id: "iitem_123", - }, -}) -``` - -*** - -## Order Module - -Medusa defines read-only links between: - -- the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `OrderLineItem` data model and the `Product` data model. Because the link is read-only from the `OrderLineItem`'s side, you can only retrieve the product of an order line item, and not the other way around. -- the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `OrderLineItem` data model and the `ProductVariant` data model. Because the link is read-only from the `OrderLineItem`'s side, you can only retrieve the variant of an order line item, and not the other way around. - -### Retrieve with Query - -To retrieve the variant of a line item with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variant.*` in `fields`: - -To retrieve the product, pass `product.*` in `fields`. - -### query.graph - -```ts -const { data: lineItems } = await query.graph({ - entity: "order_line_item", +const { data: priceSets } = await query.graph({ + entity: "price_set", fields: [ "variant.*", ], }) -// lineItems[0].variant +// priceSets[0].variant ``` ### useQueryGraphStep @@ -27384,60 +26367,14 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: lineItems } = useQueryGraphStep({ - entity: "order_line_item", +const { data: priceSets } = useQueryGraphStep({ + entity: "price_set", fields: [ "variant.*", ], }) -// lineItems[0].variant -``` - -*** - -## Pricing Module - -The Product Module doesn't provide pricing-related features. - -Instead, Medusa defines a link between the `ProductVariant` and the `PriceSet` data models. A product variant’s prices are stored belonging to a price set. - -![A diagram showcasing an example of how data models from the Pricing and Product Module are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709651464/Medusa%20Resources/product-pricing_vlxsiq.jpg) - -So, to add prices for a product variant, create a price set and add the prices to it. - -### Retrieve with Query - -To retrieve the price set of a variant with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `price_set.*` in `fields`: - -### query.graph - -```ts -const { data: variants } = await query.graph({ - entity: "variant", - fields: [ - "price_set.*", - ], -}) - -// variants[0].price_set -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: variants } = useQueryGraphStep({ - entity: "variant", - fields: [ - "price_set.*", - ], -}) - -// variants[0].price_set +// priceSets[0].variant ``` ### Manage with Link @@ -27479,31 +26416,661 @@ createRemoteLinkStep({ }) ``` + +# Prices Calculation + +In this document, you'll learn how prices are calculated when you use the [calculatePrices method](https://docs.medusajs.com/references/pricing/calculatePrices/index.html.md) of the Pricing Module's main service. + +## calculatePrices Method + +The [calculatePrices method](https://docs.medusajs.com/references/pricing/calculatePrices/index.html.md) accepts as parameters the ID of one or more price sets and a context. + +It returns a price object with the best matching price for each price set. + +### Calculation Context + +The calculation context is an optional object passed as a second parameter to the `calculatePrices` method. It accepts rules to restrict the selected prices in the price set. + +For example: + +```ts +const price = await pricingModuleService.calculatePrices( + { id: [priceSetId] }, + { + context: { + currency_code: currencyCode, + region_id: "reg_123", + }, + } +) +``` + +In this example, you retrieve the prices in a price set for the specified currency code and region ID. + +### Returned Price Object + +For each price set, the `calculatePrices` method selects two prices: + +- A calculated price: Either a price that belongs to a price list and best matches the specified context, or the same as the original price. +- An original price, which is either: + - The same price as the calculated price if the price list it belongs to is of type `override`; + - Or a price that doesn't belong to a price list and best matches the specified context. + +Both prices are returned in an object that has the following properties: + +- id: (\`string\`) The ID of the price set from which the price was selected. +- is\_calculated\_price\_price\_list: (\`boolean\`) Whether the calculated price belongs to a price list. +- calculated\_amount: (\`number\`) The amount of the calculated price, or \`null\` if there isn't a calculated price. This is the amount shown to the customer. +- is\_original\_price\_price\_list: (\`boolean\`) Whether the original price belongs to a price list. +- original\_amount: (\`number\`) The amount of the original price, or \`null\` if there isn't an original price. This amount is useful to compare with the \`calculated\_amount\`, such as to check for discounted value. +- currency\_code: (\`string\`) The currency code of the calculated price, or \`null\` if there isn't a calculated price. +- is\_calculated\_price\_tax\_inclusive: (\`boolean\`) Whether the calculated price is tax inclusive. Learn more about tax-inclusivity in \[this document]\(../tax-inclusive-pricing/page.mdx) +- is\_original\_price\_tax\_inclusive: (\`boolean\`) Whether the original price is tax inclusive. Learn more about tax-inclusivity in \[this document]\(../tax-inclusive-pricing/page.mdx) +- calculated\_price: (\`object\`) The calculated price's price details. + + - id: (\`string\`) The ID of the price. + + - price\_list\_id: (\`string\`) The ID of the associated price list. + + - price\_list\_type: (\`string\`) The price list's type. For example, \`sale\`. + + - min\_quantity: (\`number\`) The price's min quantity condition. + + - max\_quantity: (\`number\`) The price's max quantity condition. +- original\_price: (\`object\`) The original price's price details. + + - id: (\`string\`) The ID of the price. + + - price\_list\_id: (\`string\`) The ID of the associated price list. + + - price\_list\_type: (\`string\`) The price list's type. For example, \`sale\`. + + - min\_quantity: (\`number\`) The price's min quantity condition. + + - max\_quantity: (\`number\`) The price's max quantity condition. + *** -## Sales Channel Module +## Examples -The Sales Channel Module provides functionalities to manage multiple selling channels in your store. +Consider the following price set: -Medusa defines a link between the `Product` and `SalesChannel` data models. A product can have different availability in different sales channels. +```ts +const priceSet = await pricingModuleService.createPriceSets({ + prices: [ + // default price + { + amount: 500, + currency_code: "EUR", + rules: {}, + }, + // prices with rules + { + amount: 400, + currency_code: "EUR", + rules: { + region_id: "reg_123", + }, + }, + { + amount: 450, + currency_code: "EUR", + rules: { + city: "krakow", + }, + }, + { + amount: 500, + currency_code: "EUR", + rules: { + city: "warsaw", + region_id: "reg_123", + }, + }, + { + amount: 200, + currency_code: "EUR", + min_quantity: 100, + }, + ], +}) +``` -![A diagram showcasing an example of how data models from the Product and Sales Channel modules are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709651840/Medusa%20Resources/product-sales-channel_t848ik.jpg) +### Default Price Selection + +### Code + +```ts +const price = await pricingModuleService.calculatePrices( + { id: [priceSet.id] }, + { + context: { + currency_code: "EUR" + } + } +) +``` + +### Result + +### Calculate Prices with Rules + +### Code + +```ts +const price = await pricingModuleService.calculatePrices( + { id: [priceSet.id] }, + { + context: { + currency_code: "EUR", + region_id: "reg_123", + city: "krakow" + } + } +) +``` + +### Result + +### Tiered Pricing Selection + +### Code + +```ts +const price = await pricingModuleService.calculatePrices( + { id: [priceSet.id] }, + { + context: { + cart: { + items: [ + { + id: "item_1", + quantity: 200, + // assuming the price set belongs to this variant + variant_id: "variant_1", + // ... + } + ], + // ... + } + } + } +) +``` + +### Result + +### Price Selection with Price List + +### Code + +```ts +const priceList = pricingModuleService.createPriceLists([{ + title: "Summer Price List", + description: "Price list for summer sale", + starts_at: Date.parse("01/10/2023").toString(), + ends_at: Date.parse("31/10/2023").toString(), + rules: { + region_id: ['PL'] + }, + type: "sale", + prices: [ + { + amount: 400, + currency_code: "EUR", + price_set_id: priceSet.id, + }, + { + amount: 450, + currency_code: "EUR", + price_set_id: priceSet.id, + }, + ], +}]); + +const price = await pricingModuleService.calculatePrices( + { id: [priceSet.id] }, + { + context: { + currency_code: "EUR", + region_id: "PL", + city: "krakow" + } + } +) +``` + +### Result + + +# 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. + +## What is Tax-Inclusive Pricing? + +A tax-inclusive price is a price of a resource that includes taxes. Medusa calculates the tax amount from the price rather than adds the amount to it. + +For example, if a product’s price is $50, the tax rate is 2%, and tax-inclusive pricing is enabled, then the product's price is $49, and the applied tax amount is $1. + +*** + +## How is Tax-Inclusive Pricing Set? + +The [PricePreference data model](https://docs.medusajs.com/references/pricing/models/PricePreference/index.html.md) holds the tax-inclusive setting for a context. It has two properties that indicate the context: + +- `attribute`: The name of the attribute to compare against. For example, `region_id` or `currency_code`. +- `value`: The attribute’s value. For example, `reg_123` or `usd`. + +Only `region_id` and `currency_code` are supported as an `attribute` at the moment. + +The `is_tax_inclusive` property indicates whether tax-inclusivity is enabled in the specified context. + +For example: + +```json +{ + "attribute": "currency_code", + "value": "USD", + "is_tax_inclusive": true, +} +``` + +In this example, tax-inclusivity is enabled for the `USD` currency code. + +*** + +## Tax-Inclusive Pricing in Price Calculation + +### Tax Context + +As mentioned in the [Price Calculation documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md), The `calculatePrices` method accepts as a parameter a calculation context. + +To get accurate tax results, pass the `region_id` and / or `currency_code` in the calculation context. + +### Returned Tax Properties + +The `calculatePrices` method returns two properties related to tax-inclusivity: + +Learn more about the returned properties in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#returned-price-object/index.html.md). + +- `is_calculated_price_tax_inclusive`: Whether the selected `calculated_price` is tax-inclusive. +- `is_original_price_tax_inclusive` : Whether the selected `original_price` is tax-inclusive. + +A price is considered tax-inclusive if: + +1. It belongs to the region or currency code specified in the calculation context; +2. and the region or currency code has a price preference with `is_tax_inclusive` enabled. + +### Tax Context Precedence + +A region’s price preference’s `is_tax_inclusive`'s value takes higher precedence in determining whether a price is tax-inclusive if: + +- both the `region_id` and `currency_code` are provided in the calculation context; +- the selected price belongs to the region; +- and the region has a price preference + + +# Price Tiers and Rules + +In this Pricing Module guide, you'll learn about tired prices, price rules for price sets and price lists, and how to add rules to a price. + +## Tiered Pricing + +Each price, represented by the [Price data model](https://docs.medusajs.com/references/pricing/models/Price/index.html.md), has two optional properties that can be used to create tiered prices: + +- `min_quantity`: The minimum quantity that must be in the cart for the price to be applied. +- `max_quantity`: The maximum quantity that can be in the cart for the price to be applied. + +This is useful to set tiered pricing for resources like product variants and shipping options. + +For example, you can set a variant's price to: + +- `$10` by default. +- `$8` when the customer adds `10` or more of the variant to the cart. +- `$6` when the customer adds `20` or more of the variant to the cart. + +These price definitions would look like this: + +```json title="Example Prices" +[ + // default price + { + "amount": 10, + "currency_code": "usd", + }, + { + "amount": 8, + "currency_code": "usd", + "min_quantity": 10, + "max_quantity": 19, + }, + { + "amount": 6, + "currency_code": "usd", + "min_quantity": 20, + }, +], +``` + +### How to Create Tiered Prices? + +When you create prices, you can specify a `min_quantity` and `max_quantity` for each price. This allows you to create tiered pricing, where the price changes based on the quantity of items in the cart. + +For example: + +For most use cases where you're building customizations in the Medusa application, it's highly recommended to use [Medusa's workflows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/medusa-workflows-reference/index.html.md) rather than using the Pricing Module directly. Medusa's workflows already implement extensive functionalities that you can re-use in your custom flows, with reliable roll-back mechanism. + +### Using Medusa Workflows + +```ts highlights={tieredPricingHighlights} +const { result } = await createProductsWorkflow(container) + .run({ + input: { + products: [{ + variants: [{ + id: "variant_1", + prices: [ + // default price + { + amount: 10, + currency_code: "usd", + }, + { + amount: 8, + currency_code: "usd", + min_quantity: 10, + max_quantity: 19, + }, + { + amount: 6, + currency_code: "usd", + min_quantity: 20, + }, + ], + // ... + }], + }], + // ... + }, + }) +``` + +### Using the Pricing Module + +```ts +const priceSet = await pricingModule.addPrices({ + priceSetId: "pset_1", + prices: [ + // default price + { + amount: 10, + currency_code: "usd", + }, + // tiered prices + { + amount: 8, + currency_code: "usd", + min_quantity: 10, + max_quantity: 19, + }, + { + amount: 6, + currency_code: "usd", + min_quantity: 20, + }, + ], +}) +``` + +In this example, you create a product with a variant whose default price is `$10`. You also add two tiered prices that set the price to `$8` when the quantity is between `10` and `19`, and to `$6` when the quantity is `20` or more. + +### How are Tiered Prices Applied? + +The [price calculation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md) mechanism considers the cart's items as a context when choosing the best price to apply. + +For example, consider the customer added the `variant_1` product variant (created in the workflow snippet of the [above section](#how-to-create-tiered-prices)) to their cart with a quantity of `15`. + +The price calculation mechanism will choose the second price, which is `$8`, because the quantity of `15` is between `10` and `19`. + +If there are other rules applied to the price, they may affect the price calculation. Keep reading to learn about other price rules, and refer to the [Price Calculation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md) guide for more details on the calculation mechanism. + +*** + +## Price Rule + +You can also restrict prices by advanced rules, such as a customer's group, zip code, or a cart's total. + +Each rule of a price is represented by the [PriceRule data model](https://docs.medusajs.com/references/pricing/models/PriceRule/index.html.md). + +The `Price` data model has a `rules_count` property, which indicates how many rules, represented by `PriceRule`, are applied to the price. + +For exmaple, you create a price restricted to `10557` zip codes. + +![A diagram showcasing the relation between the PriceRule and Price](https://res.cloudinary.com/dza7lstvk/image/upload/v1709648772/Medusa%20Resources/price-rule-1_vy8bn9.jpg) + +A price can have multiple price rules. + +For example, a price can be restricted by a region and a zip code. + +![A diagram showcasing the relation between the PriceRule and Price with multiple rules.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709649296/Medusa%20Resources/price-rule-3_pwpocz.jpg) + +### Price List Rules + +Rules applied to a price list are represented by the [PriceListRule data model](https://docs.medusajs.com/references/pricing/models/PriceListRule/index.html.md). + +The `rules_count` property of a `PriceList` indicates how many rules are applied to it. + +![A diagram showcasing the relation between the PriceSet, PriceList, Price, RuleType, and PriceListRuleValue](https://res.cloudinary.com/dza7lstvk/image/upload/v1709641999/Medusa%20Resources/price-list_zd10yd.jpg) + +### How to Create Prices with Rules? + +When you create prices, you can specify rules for each price. This allows you to create complex pricing strategies based on different contexts. + +For example: + +For most use cases where you're building customizations in the Medusa application, it's highly recommended to use [Medusa's workflows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/medusa-workflows-reference/index.html.md) rather than using the Pricing Module directly. Medusa's workflows already implement extensive functionalities that you can re-use in your custom flows, with reliable roll-back mechanism. + +### Using Medusa Workflows + +```ts highlights={workflowHighlights} +const { result } = await createShippingOptionsWorkflow(container) + .run({ + input: [{ + name: "Standard Shipping", + service_zone_id: "serzo_123", + shipping_profile_id: "sp_123", + provider_id: "prov_123", + type: { + label: "Standard", + description: "Standard shipping", + code: "standard", + }, + price_type: "flat", + prices: [ + // default price + { + currency_code: "usd", + amount: 10, + rules: {}, + }, + // price if cart total >= $100 + { + currency_code: "usd", + amount: 0, + rules: { + item_total: { + operator: "gte", + value: 100, + }, + }, + }, + ], + }], + }) +``` + +### Using the Pricing Module + +```ts +const priceSet = await pricingModule.addPrices({ + priceSetId: "pset_1", + prices: [ + // default price + { + currency_code: "usd", + amount: 10, + rules: {}, + }, + // price if cart total >= $100 + { + currency_code: "usd", + amount: 0, + rules: { + item_total: { + operator: "gte", + value: 100, + }, + }, + }, + ], +}) +``` + +In this example, you create a shipping option whose default price is `$10`. When the total of the cart or order using this shipping option is greater than `$100`, the shipping option's price becomes free. + +### How is the Price Rule Applied? + +The [price calculation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md) mechanism considers a price applicable when the resource that this price is in matches the specified rules. + +For example, a [cart object](https://docs.medusajs.com/api/store#carts_cart_schema) has an `item_total` property. So, if a shipping option has the following price: + +```json +{ + "currency_code": "usd", + "amount": 0, + "rules": { + "item_total": { + "operator": "gte", + "value": 100, + } + } +} +``` + +The shipping option's price is applied when the cart's `item_total` is greater than or equal to `$100`. + +You can also apply the rule on nested relations and properties. For example, to apply a shipping option's price based on the customer's group, you can apply a rule on the `customer.group.id` attribute: + +```json +{ + "currency_code": "usd", + "amount": 0, + "rules": { + "customer.group.id": { + "operator": "eq", + "value": "cusgrp_123" + } + } +} +``` + +In this example, the price is only applied if a cart's customer belongs to the customer group of ID `cusgrp_123`. + +These same rules apply to product variant prices as well, or any other resource that has a price. + + +# 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 sales channels of a product with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channels.*` in `fields`: +To retrieve the 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: products } = await query.graph({ - entity: "product", +const { data: paymentCollections } = await query.graph({ + entity: "payment_collection", fields: [ - "sales_channels.*", + "cart.*", ], }) -// products[0].sales_channels +// paymentCollections[0].cart ``` ### useQueryGraphStep @@ -27513,19 +27080,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: products } = useQueryGraphStep({ - entity: "product", +const { data: paymentCollections } = useQueryGraphStep({ + entity: "payment_collection", fields: [ - "sales_channels.*", + "cart.*", ], }) -// products[0].sales_channels +// paymentCollections[0].cart ``` ### Manage with Link -To manage the sales channels of a product, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +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 @@ -27535,11 +27102,173 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.PRODUCT]: { - product_id: "prod_123", + [Modules.CART]: { + cart_id: "cart_123", }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_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", }, }) ``` @@ -27553,255 +27282,40 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.PRODUCT]: { - product_id: "prod_123", + [Modules.ORDER]: { + order_id: "order_123", }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", + [Modules.PAYMENT]: { + payment_collection_id: "paycol_123", }, }) ``` - -# 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 +## Region Module -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). - - -# 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.||| - - -# 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. +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 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`: +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: regions } = await query.graph({ - entity: "region", +const { data: paymentProviders } = await query.graph({ + entity: "payment_provider", fields: [ - "payment_providers.*", + "regions.*", ], }) -// regions[0].payment_providers +// paymentProviders[0].regions ``` ### useQueryGraphStep @@ -27811,14 +27325,14 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: regions } = useQueryGraphStep({ - entity: "region", +const { data: paymentProviders } = useQueryGraphStep({ + entity: "payment_provider", fields: [ - "payment_providers.*", + "regions.*", ], }) -// regions[0].payment_providers +// paymentProviders[0].regions ``` ### Manage with Link @@ -27861,6 +27375,564 @@ 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). @@ -28380,6 +28452,750 @@ createRemoteLinkStep({ ``` +# Links between Product Module and Other Modules + +This document showcases the module links defined between the Product Module and other Commerce Modules. + +## Summary + +The Product Module has the following links to other modules: + +Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. + +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +|LineItem|Product|Read-only - has one|Learn more| +|Product|ShippingProfile|Stored - many-to-one|Learn more| +|ProductVariant|InventoryItem|Stored - many-to-many|Learn more| +|OrderLineItem|Product|Read-only - has one|Learn more| +|ProductVariant|PriceSet|Stored - one-to-one|Learn more| +|Product|SalesChannel|Stored - many-to-many|Learn more| + +*** + +## Cart Module + +Medusa defines read-only links between: + +- The [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `LineItem` data model and the `Product` data model. Because the link is read-only from the `LineItem`'s side, you can only retrieve the product of a line item, and not the other way around. +- The `ProductVariant` data model and the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `LineItem` data model. Because the link is read-only from the `LineItem`'s side, you can only retrieve the variant of a line item, and not the other way around. + +### 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[0].variant +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: lineItems } = useQueryGraphStep({ + entity: "line_item", + fields: [ + "variant.*", + ], +}) + +// lineItems[0].variant +``` + +*** + +## Fulfillment Module + +Medusa defines a link between the `Product` data model and the `ShippingProfile` data model of the Fulfillment Module. Each product must belong to a shipping profile. + +This link is introduced in [Medusa v2.5.0](https://github.com/medusajs/medusa/releases/tag/v2.5.0). + +### Retrieve with Query + +To retrieve the shipping profile of a product with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `shipping_profile.*` in `fields`: + +### query.graph + +```ts +const { data: products } = await query.graph({ + entity: "product", + fields: [ + "shipping_profile.*", + ], +}) + +// products[0].shipping_profile +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: products } = useQueryGraphStep({ + entity: "product", + fields: [ + "shipping_profile.*", + ], +}) + +// products[0].shipping_profile +``` + +### Manage with Link + +To manage the shipping profile of a product, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + [Modules.FULFILLMENT]: { + shipping_profile_id: "sp_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + [Modules.FULFILLMENT]: { + shipping_profile_id: "sp_123", + }, +}) +``` + +*** + +## Inventory Module + +The Inventory Module provides inventory-management features for any stock-kept item. + +Medusa defines a link between the `ProductVariant` and `InventoryItem` data models. Each product variant has different inventory details. + +![A diagram showcasing an example of how data models from the Product and Inventory modules are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709652779/Medusa%20Resources/product-inventory_kmjnud.jpg) + +When the `manage_inventory` property of a product variant is enabled, you can manage the variant's inventory in different locations through this relation. + +Learn more about product variant's inventory management in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/variant-inventory/index.html.md). + +### Retrieve with Query + +To retrieve the inventory items of a product variant with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `inventory_items.*` in `fields`: + +### query.graph + +```ts +const { data: variants } = await query.graph({ + entity: "variant", + fields: [ + "inventory_items.*", + ], +}) + +// variants[0].inventory_items +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: variants } = useQueryGraphStep({ + entity: "variant", + fields: [ + "inventory_items.*", + ], +}) + +// variants[0].inventory_items +``` + +### Manage with Link + +To manage the inventory items of a variant, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.PRODUCT]: { + variant_id: "variant_123", + }, + [Modules.INVENTORY]: { + inventory_item_id: "iitem_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.PRODUCT]: { + variant_id: "variant_123", + }, + [Modules.INVENTORY]: { + inventory_item_id: "iitem_123", + }, +}) +``` + +*** + +## Order Module + +Medusa defines read-only links between: + +- the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `OrderLineItem` data model and the `Product` data model. Because the link is read-only from the `OrderLineItem`'s side, you can only retrieve the product of an order line item, and not the other way around. +- the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `OrderLineItem` data model and the `ProductVariant` data model. Because the link is read-only from the `OrderLineItem`'s side, you can only retrieve the variant of an order line item, and not the other way around. + +### Retrieve with Query + +To retrieve the variant of a line item with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variant.*` in `fields`: + +To retrieve the product, pass `product.*` in `fields`. + +### query.graph + +```ts +const { data: lineItems } = await query.graph({ + entity: "order_line_item", + fields: [ + "variant.*", + ], +}) + +// lineItems[0].variant +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: lineItems } = useQueryGraphStep({ + entity: "order_line_item", + fields: [ + "variant.*", + ], +}) + +// lineItems[0].variant +``` + +*** + +## Pricing Module + +The Product Module doesn't provide pricing-related features. + +Instead, Medusa defines a link between the `ProductVariant` and the `PriceSet` data models. A product variant’s prices are stored belonging to a price set. + +![A diagram showcasing an example of how data models from the Pricing and Product Module are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709651464/Medusa%20Resources/product-pricing_vlxsiq.jpg) + +So, to add prices for a product variant, create a price set and add the prices to it. + +### Retrieve with Query + +To retrieve the price set of a variant with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `price_set.*` in `fields`: + +### query.graph + +```ts +const { data: variants } = await query.graph({ + entity: "variant", + fields: [ + "price_set.*", + ], +}) + +// variants[0].price_set +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: variants } = useQueryGraphStep({ + entity: "variant", + fields: [ + "price_set.*", + ], +}) + +// variants[0].price_set +``` + +### Manage with Link + +To manage the price set of a variant, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.PRODUCT]: { + variant_id: "variant_123", + }, + [Modules.PRICING]: { + price_set_id: "pset_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.PRODUCT]: { + variant_id: "variant_123", + }, + [Modules.PRICING]: { + price_set_id: "pset_123", + }, +}) +``` + +*** + +## Sales Channel Module + +The Sales Channel Module provides functionalities to manage multiple selling channels in your store. + +Medusa defines a link between the `Product` and `SalesChannel` data models. A product can have different availability in different sales channels. + +![A diagram showcasing an example of how data models from the Product and Sales Channel modules are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709651840/Medusa%20Resources/product-sales-channel_t848ik.jpg) + +### Retrieve with Query + +To retrieve the sales channels of a product with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channels.*` in `fields`: + +### query.graph + +```ts +const { data: products } = await query.graph({ + entity: "product", + fields: [ + "sales_channels.*", + ], +}) + +// products[0].sales_channels +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: products } = useQueryGraphStep({ + entity: "product", + fields: [ + "sales_channels.*", + ], +}) + +// products[0].sales_channels +``` + +### Manage with Link + +To manage the sales channels of a product, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, +}) +``` + + +# 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. @@ -28783,774 +29599,21 @@ 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. -# Pricing Concepts +# Stock Location Concepts -In this document, you’ll learn about the main concepts in the Pricing Module. +In this document, you’ll learn about the main concepts in the Stock Location Module. -## Price Set +## Stock Location -A [PriceSet](https://docs.medusajs.com/references/pricing/models/PriceSet/index.html.md) represents a collection of prices that are linked to a resource (for example, a product or a shipping option). +A stock location, represented by the `StockLocation` data model, represents a location where stock items are kept. For example, a warehouse. -Each of these prices are represented by the [Price data module](https://docs.medusajs.com/references/pricing/models/Price/index.html.md). - -![A diagram showcasing the relation between the price set and price](https://res.cloudinary.com/dza7lstvk/image/upload/v1709648650/Medusa%20Resources/price-set-money-amount_xeees0.jpg) +Medusa uses stock locations to provide inventory details, from the Inventory Module, per location. *** -## Price List +## StockLocationAddress -A [PriceList](https://docs.medusajs.com/references/pricing/models/PriceList/index.html.md) is a group of prices only enabled if their conditions and rules are satisfied. - -A price list has optional `start_date` and `end_date` properties that indicate the date range in which a price list can be applied. - -Its associated prices are represented by the `Price` data model. - - -# Links between Pricing Module and Other Modules - -This document showcases the module links defined between the Pricing Module and other Commerce Modules. - -## Summary - -The Pricing Module has the following links to other modules: - -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -|ShippingOption|PriceSet|Stored - one-to-one|Learn more| -|ProductVariant|PriceSet|Stored - one-to-one|Learn more| - -*** - -## Fulfillment Module - -The Fulfillment Module provides fulfillment-related functionalities, including shipping options that the customer chooses from when they place their order. However, it doesn't provide pricing-related functionalities for these options. - -Medusa defines a link between the `PriceSet` and `ShippingOption` data models. A shipping option's price is stored as a price set. - -![A diagram showcasing an example of how data models from the Pricing and Fulfillment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716561747/Medusa%20Resources/pricing-fulfillment_spywwa.jpg) - -### Retrieve with Query - -To retrieve the shipping option of a price set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `shipping_option.*` in `fields`: - -### query.graph - -```ts -const { data: priceSets } = await query.graph({ - entity: "price_set", - fields: [ - "shipping_option.*", - ], -}) - -// priceSets[0].shipping_option -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: priceSets } = useQueryGraphStep({ - entity: "price_set", - fields: [ - "shipping_option.*", - ], -}) - -// priceSets[0].shipping_option -``` - -### Manage with Link - -To manage the price set of a shipping option, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.FULFILLMENT]: { - shipping_option_id: "so_123", - }, - [Modules.PRICING]: { - price_set_id: "pset_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.FULFILLMENT]: { - shipping_option_id: "so_123", - }, - [Modules.PRICING]: { - price_set_id: "pset_123", - }, -}) -``` - -*** - -## Product Module - -The Product Module doesn't store or manage the prices of product variants. - -Medusa defines a link between the `ProductVariant` and the `PriceSet`. A product variant’s prices are stored as prices belonging to a price set. - -![A diagram showcasing an example of how data models from the Pricing and Product Module are linked. The PriceSet is linked to the ProductVariant of the Product Module.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709651039/Medusa%20Resources/pricing-product_m4xaut.jpg) - -So, when you want to add prices for a product variant, you create a price set and add the prices to it. - -You can then benefit from adding rules to prices or using the `calculatePrices` method to retrieve the price of a product variant within a specified context. - -### Retrieve with Query - -To retrieve the variant of a price set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variant.*` in `fields`: - -### query.graph - -```ts -const { data: priceSets } = await query.graph({ - entity: "price_set", - fields: [ - "variant.*", - ], -}) - -// priceSets[0].variant -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: priceSets } = useQueryGraphStep({ - entity: "price_set", - fields: [ - "variant.*", - ], -}) - -// priceSets[0].variant -``` - -### Manage with Link - -To manage the price set of a variant, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.PRODUCT]: { - variant_id: "variant_123", - }, - [Modules.PRICING]: { - price_set_id: "pset_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.PRODUCT]: { - variant_id: "variant_123", - }, - [Modules.PRICING]: { - price_set_id: "pset_123", - }, -}) -``` - - -# Price Tiers and Rules - -In this Pricing Module guide, you'll learn about tired prices, price rules for price sets and price lists, and how to add rules to a price. - -## Tiered Pricing - -Each price, represented by the [Price data model](https://docs.medusajs.com/references/pricing/models/Price/index.html.md), has two optional properties that can be used to create tiered prices: - -- `min_quantity`: The minimum quantity that must be in the cart for the price to be applied. -- `max_quantity`: The maximum quantity that can be in the cart for the price to be applied. - -This is useful to set tiered pricing for resources like product variants and shipping options. - -For example, you can set a variant's price to: - -- `$10` by default. -- `$8` when the customer adds `10` or more of the variant to the cart. -- `$6` when the customer adds `20` or more of the variant to the cart. - -These price definitions would look like this: - -```json title="Example Prices" -[ - // default price - { - "amount": 10, - "currency_code": "usd", - }, - { - "amount": 8, - "currency_code": "usd", - "min_quantity": 10, - "max_quantity": 19, - }, - { - "amount": 6, - "currency_code": "usd", - "min_quantity": 20, - }, -], -``` - -### How to Create Tiered Prices? - -When you create prices, you can specify a `min_quantity` and `max_quantity` for each price. This allows you to create tiered pricing, where the price changes based on the quantity of items in the cart. - -For example: - -For most use cases where you're building customizations in the Medusa application, it's highly recommended to use [Medusa's workflows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/medusa-workflows-reference/index.html.md) rather than using the Pricing Module directly. Medusa's workflows already implement extensive functionalities that you can re-use in your custom flows, with reliable roll-back mechanism. - -### Using Medusa Workflows - -```ts highlights={tieredPricingHighlights} -const { result } = await createProductsWorkflow(container) - .run({ - input: { - products: [{ - variants: [{ - id: "variant_1", - prices: [ - // default price - { - amount: 10, - currency_code: "usd", - }, - { - amount: 8, - currency_code: "usd", - min_quantity: 10, - max_quantity: 19, - }, - { - amount: 6, - currency_code: "usd", - min_quantity: 20, - }, - ], - // ... - }], - }], - // ... - }, - }) -``` - -### Using the Pricing Module - -```ts -const priceSet = await pricingModule.addPrices({ - priceSetId: "pset_1", - prices: [ - // default price - { - amount: 10, - currency_code: "usd", - }, - // tiered prices - { - amount: 8, - currency_code: "usd", - min_quantity: 10, - max_quantity: 19, - }, - { - amount: 6, - currency_code: "usd", - min_quantity: 20, - }, - ], -}) -``` - -In this example, you create a product with a variant whose default price is `$10`. You also add two tiered prices that set the price to `$8` when the quantity is between `10` and `19`, and to `$6` when the quantity is `20` or more. - -### How are Tiered Prices Applied? - -The [price calculation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md) mechanism considers the cart's items as a context when choosing the best price to apply. - -For example, consider the customer added the `variant_1` product variant (created in the workflow snippet of the [above section](#how-to-create-tiered-prices)) to their cart with a quantity of `15`. - -The price calculation mechanism will choose the second price, which is `$8`, because the quantity of `15` is between `10` and `19`. - -If there are other rules applied to the price, they may affect the price calculation. Keep reading to learn about other price rules, and refer to the [Price Calculation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md) guide for more details on the calculation mechanism. - -*** - -## Price Rule - -You can also restrict prices by advanced rules, such as a customer's group, zip code, or a cart's total. - -Each rule of a price is represented by the [PriceRule data model](https://docs.medusajs.com/references/pricing/models/PriceRule/index.html.md). - -The `Price` data model has a `rules_count` property, which indicates how many rules, represented by `PriceRule`, are applied to the price. - -For exmaple, you create a price restricted to `10557` zip codes. - -![A diagram showcasing the relation between the PriceRule and Price](https://res.cloudinary.com/dza7lstvk/image/upload/v1709648772/Medusa%20Resources/price-rule-1_vy8bn9.jpg) - -A price can have multiple price rules. - -For example, a price can be restricted by a region and a zip code. - -![A diagram showcasing the relation between the PriceRule and Price with multiple rules.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709649296/Medusa%20Resources/price-rule-3_pwpocz.jpg) - -### Price List Rules - -Rules applied to a price list are represented by the [PriceListRule data model](https://docs.medusajs.com/references/pricing/models/PriceListRule/index.html.md). - -The `rules_count` property of a `PriceList` indicates how many rules are applied to it. - -![A diagram showcasing the relation between the PriceSet, PriceList, Price, RuleType, and PriceListRuleValue](https://res.cloudinary.com/dza7lstvk/image/upload/v1709641999/Medusa%20Resources/price-list_zd10yd.jpg) - -### How to Create Prices with Rules? - -When you create prices, you can specify rules for each price. This allows you to create complex pricing strategies based on different contexts. - -For example: - -For most use cases where you're building customizations in the Medusa application, it's highly recommended to use [Medusa's workflows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/medusa-workflows-reference/index.html.md) rather than using the Pricing Module directly. Medusa's workflows already implement extensive functionalities that you can re-use in your custom flows, with reliable roll-back mechanism. - -### Using Medusa Workflows - -```ts highlights={workflowHighlights} -const { result } = await createShippingOptionsWorkflow(container) - .run({ - input: [{ - name: "Standard Shipping", - service_zone_id: "serzo_123", - shipping_profile_id: "sp_123", - provider_id: "prov_123", - type: { - label: "Standard", - description: "Standard shipping", - code: "standard", - }, - price_type: "flat", - prices: [ - // default price - { - currency_code: "usd", - amount: 10, - rules: {}, - }, - // price if cart total >= $100 - { - currency_code: "usd", - amount: 0, - rules: { - item_total: { - operator: "gte", - value: 100, - }, - }, - }, - ], - }], - }) -``` - -### Using the Pricing Module - -```ts -const priceSet = await pricingModule.addPrices({ - priceSetId: "pset_1", - prices: [ - // default price - { - currency_code: "usd", - amount: 10, - rules: {}, - }, - // price if cart total >= $100 - { - currency_code: "usd", - amount: 0, - rules: { - item_total: { - operator: "gte", - value: 100, - }, - }, - }, - ], -}) -``` - -In this example, you create a shipping option whose default price is `$10`. When the total of the cart or order using this shipping option is greater than `$100`, the shipping option's price becomes free. - -### How is the Price Rule Applied? - -The [price calculation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md) mechanism considers a price applicable when the resource that this price is in matches the specified rules. - -For example, a [cart object](https://docs.medusajs.com/api/store#carts_cart_schema) has an `item_total` property. So, if a shipping option has the following price: - -```json -{ - "currency_code": "usd", - "amount": 0, - "rules": { - "item_total": { - "operator": "gte", - "value": 100, - } - } -} -``` - -The shipping option's price is applied when the cart's `item_total` is greater than or equal to `$100`. - -You can also apply the rule on nested relations and properties. For example, to apply a shipping option's price based on the customer's group, you can apply a rule on the `customer.group.id` attribute: - -```json -{ - "currency_code": "usd", - "amount": 0, - "rules": { - "customer.group.id": { - "operator": "eq", - "value": "cusgrp_123" - } - } -} -``` - -In this example, the price is only applied if a cart's customer belongs to the customer group of ID `cusgrp_123`. - -These same rules apply to product variant prices as well, or any other resource that has a price. - - -# Prices Calculation - -In this document, you'll learn how prices are calculated when you use the [calculatePrices method](https://docs.medusajs.com/references/pricing/calculatePrices/index.html.md) of the Pricing Module's main service. - -## calculatePrices Method - -The [calculatePrices method](https://docs.medusajs.com/references/pricing/calculatePrices/index.html.md) accepts as parameters the ID of one or more price sets and a context. - -It returns a price object with the best matching price for each price set. - -### Calculation Context - -The calculation context is an optional object passed as a second parameter to the `calculatePrices` method. It accepts rules to restrict the selected prices in the price set. - -For example: - -```ts -const price = await pricingModuleService.calculatePrices( - { id: [priceSetId] }, - { - context: { - currency_code: currencyCode, - region_id: "reg_123", - }, - } -) -``` - -In this example, you retrieve the prices in a price set for the specified currency code and region ID. - -### Returned Price Object - -For each price set, the `calculatePrices` method selects two prices: - -- A calculated price: Either a price that belongs to a price list and best matches the specified context, or the same as the original price. -- An original price, which is either: - - The same price as the calculated price if the price list it belongs to is of type `override`; - - Or a price that doesn't belong to a price list and best matches the specified context. - -Both prices are returned in an object that has the following properties: - -- id: (\`string\`) The ID of the price set from which the price was selected. -- is\_calculated\_price\_price\_list: (\`boolean\`) Whether the calculated price belongs to a price list. -- calculated\_amount: (\`number\`) The amount of the calculated price, or \`null\` if there isn't a calculated price. This is the amount shown to the customer. -- is\_original\_price\_price\_list: (\`boolean\`) Whether the original price belongs to a price list. -- original\_amount: (\`number\`) The amount of the original price, or \`null\` if there isn't an original price. This amount is useful to compare with the \`calculated\_amount\`, such as to check for discounted value. -- currency\_code: (\`string\`) The currency code of the calculated price, or \`null\` if there isn't a calculated price. -- is\_calculated\_price\_tax\_inclusive: (\`boolean\`) Whether the calculated price is tax inclusive. Learn more about tax-inclusivity in \[this document]\(../tax-inclusive-pricing/page.mdx) -- is\_original\_price\_tax\_inclusive: (\`boolean\`) Whether the original price is tax inclusive. Learn more about tax-inclusivity in \[this document]\(../tax-inclusive-pricing/page.mdx) -- calculated\_price: (\`object\`) The calculated price's price details. - - - id: (\`string\`) The ID of the price. - - - price\_list\_id: (\`string\`) The ID of the associated price list. - - - price\_list\_type: (\`string\`) The price list's type. For example, \`sale\`. - - - min\_quantity: (\`number\`) The price's min quantity condition. - - - max\_quantity: (\`number\`) The price's max quantity condition. -- original\_price: (\`object\`) The original price's price details. - - - id: (\`string\`) The ID of the price. - - - price\_list\_id: (\`string\`) The ID of the associated price list. - - - price\_list\_type: (\`string\`) The price list's type. For example, \`sale\`. - - - min\_quantity: (\`number\`) The price's min quantity condition. - - - max\_quantity: (\`number\`) The price's max quantity condition. - -*** - -## Examples - -Consider the following price set: - -```ts -const priceSet = await pricingModuleService.createPriceSets({ - prices: [ - // default price - { - amount: 500, - currency_code: "EUR", - rules: {}, - }, - // prices with rules - { - amount: 400, - currency_code: "EUR", - rules: { - region_id: "reg_123", - }, - }, - { - amount: 450, - currency_code: "EUR", - rules: { - city: "krakow", - }, - }, - { - amount: 500, - currency_code: "EUR", - rules: { - city: "warsaw", - region_id: "reg_123", - }, - }, - { - amount: 200, - currency_code: "EUR", - min_quantity: 100, - }, - ], -}) -``` - -### Default Price Selection - -### Code - -```ts -const price = await pricingModuleService.calculatePrices( - { id: [priceSet.id] }, - { - context: { - currency_code: "EUR" - } - } -) -``` - -### Result - -### Calculate Prices with Rules - -### Code - -```ts -const price = await pricingModuleService.calculatePrices( - { id: [priceSet.id] }, - { - context: { - currency_code: "EUR", - region_id: "reg_123", - city: "krakow" - } - } -) -``` - -### Result - -### Tiered Pricing Selection - -### Code - -```ts -const price = await pricingModuleService.calculatePrices( - { id: [priceSet.id] }, - { - context: { - cart: { - items: [ - { - id: "item_1", - quantity: 200, - // assuming the price set belongs to this variant - variant_id: "variant_1", - // ... - } - ], - // ... - } - } - } -) -``` - -### Result - -### Price Selection with Price List - -### Code - -```ts -const priceList = pricingModuleService.createPriceLists([{ - title: "Summer Price List", - description: "Price list for summer sale", - starts_at: Date.parse("01/10/2023").toString(), - ends_at: Date.parse("31/10/2023").toString(), - rules: { - region_id: ['PL'] - }, - type: "sale", - prices: [ - { - amount: 400, - currency_code: "EUR", - price_set_id: priceSet.id, - }, - { - amount: 450, - currency_code: "EUR", - price_set_id: priceSet.id, - }, - ], -}]); - -const price = await pricingModuleService.calculatePrices( - { id: [priceSet.id] }, - { - context: { - currency_code: "EUR", - region_id: "PL", - city: "krakow" - } - } -) -``` - -### Result - - -# Tax-Inclusive Pricing - -In this document, you’ll learn about tax-inclusive pricing and how it's used when calculating prices. - -## What is Tax-Inclusive Pricing? - -A tax-inclusive price is a price of a resource that includes taxes. Medusa calculates the tax amount from the price rather than adds the amount to it. - -For example, if a product’s price is $50, the tax rate is 2%, and tax-inclusive pricing is enabled, then the product's price is $49, and the applied tax amount is $1. - -*** - -## How is Tax-Inclusive Pricing Set? - -The [PricePreference data model](https://docs.medusajs.com/references/pricing/models/PricePreference/index.html.md) holds the tax-inclusive setting for a context. It has two properties that indicate the context: - -- `attribute`: The name of the attribute to compare against. For example, `region_id` or `currency_code`. -- `value`: The attribute’s value. For example, `reg_123` or `usd`. - -Only `region_id` and `currency_code` are supported as an `attribute` at the moment. - -The `is_tax_inclusive` property indicates whether tax-inclusivity is enabled in the specified context. - -For example: - -```json -{ - "attribute": "currency_code", - "value": "USD", - "is_tax_inclusive": true, -} -``` - -In this example, tax-inclusivity is enabled for the `USD` currency code. - -*** - -## Tax-Inclusive Pricing in Price Calculation - -### Tax Context - -As mentioned in the [Price Calculation documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md), The `calculatePrices` method accepts as a parameter a calculation context. - -To get accurate tax results, pass the `region_id` and / or `currency_code` in the calculation context. - -### Returned Tax Properties - -The `calculatePrices` method returns two properties related to tax-inclusivity: - -Learn more about the returned properties in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#returned-price-object/index.html.md). - -- `is_calculated_price_tax_inclusive`: Whether the selected `calculated_price` is tax-inclusive. -- `is_original_price_tax_inclusive` : Whether the selected `original_price` is tax-inclusive. - -A price is considered tax-inclusive if: - -1. It belongs to the region or currency code specified in the calculation context; -2. and the region or currency code has a price preference with `is_tax_inclusive` enabled. - -### Tax Context Precedence - -A region’s price preference’s `is_tax_inclusive`'s value takes higher precedence in determining whether a price is tax-inclusive if: - -- both the `region_id` and `currency_code` are provided in the calculation context; -- the selected price belongs to the region; -- and the region has a price preference +The `StockLocationAddress` data model belongs to the `StockLocation` data model. It provides more detailed information of the location, such as country code or street address. # Links between Stock Location Module and Other Modules @@ -29783,21 +29846,178 @@ createRemoteLinkStep({ ``` -# Stock Location Concepts +# User Module Options -In this document, you’ll learn about the main concepts in the Stock Location Module. +In this document, you'll learn about the options of the User Module. -## Stock Location +## Module Options -A stock location, represented by the `StockLocation` data model, represents a location where stock items are kept. For example, a warehouse. +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" -Medusa uses stock locations to provide inventory details, from the Inventory Module, per location. +// ... + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/user", + options: { + jwt_secret: process.env.JWT_SECRET, + }, + }, + ], +}) +``` + +|Option|Description|Required| +|---|---|---|---|---| +|\`jwt\_secret\`|A string indicating the secret used to sign the invite tokens.|Yes| + +### Environment Variables + +Make sure to add the necessary environment variables for the above options in `.env`: + +```bash +JWT_SECRET=supersecret +``` + + +# User Creation Flows + +In this document, learn the different ways to create a user using the User Module. + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/users/index.html.md) to learn how to manage users using the dashboard. + +## Straightforward User Creation + +To create a user, use the [create method of the User Module’s main service](https://docs.medusajs.com/references/user/create/index.html.md): + +```ts +const user = await userModuleService.createUsers({ + email: "user@example.com", +}) +``` + +You can pair this with the Auth Module to allow the user to authenticate, as explained in a [later section](#create-identity-with-the-auth-module). *** -## StockLocationAddress +## Invite Users -The `StockLocationAddress` data model belongs to the `StockLocation` data model. It provides more detailed information of the location, such as country code or street address. +To create a user, you can create an invite for them using the [createInvites method](https://docs.medusajs.com/references/user/createInvites/index.html.md) of the User Module's main service: + +```ts +const invite = await userModuleService.createInvites({ + email: "user@example.com", +}) +``` + +Later, you can accept the invite and create a new user for them: + +```ts +const invite = + await userModuleService.validateInviteToken("secret_123") + +await userModuleService.updateInvites({ + id: invite.id, + accepted: true, +}) + +const user = await userModuleService.createUsers({ + email: invite.email, +}) +``` + +### Invite Expiry + +An invite has an expiry date. You can renew the expiry date and refresh the token using the [refreshInviteTokens method](https://docs.medusajs.com/references/user/refreshInviteTokens/index.html.md): + +```ts +await userModuleService.refreshInviteTokens(["invite_123"]) +``` + +*** + +## Create Identity with the Auth Module + +By combining the User and Auth Modules, you can use the Auth Module for authenticating users, and the User Module to manage those users. + +So, when a user is authenticated, and you receive the `AuthIdentity` object, you can use it to create a user if it doesn’t exist: + +```ts +const { success, authIdentity } = + await authModuleService.authenticate("emailpass", { + // ... + }) + +const [, count] = await userModuleService.listAndCountUsers({ + email: authIdentity.entity_id, +}) + +if (!count) { + const user = await userModuleService.createUsers({ + email: authIdentity.entity_id, + }) +} +``` + + +# 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 @@ -29986,41 +30206,6 @@ You can remove a registered tax provider from the Medusa application by removing Then, the next time the Medusa application starts, it will set the `is_enabled` property of the `TaxProvider`'s record to `false`. This allows you to re-enable the tax provider later if needed by adding it back to the `providers` option. -# Tax Region - -In this document, you’ll learn about tax regions and how to use them with the Region Module. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md) to learn how to manage tax regions using the dashboard. - -## What is a Tax Region? - -A tax region, represented by the [TaxRegion data model](https://docs.medusajs.com/references/tax/models/TaxRegion/index.html.md), stores tax settings related to a region that your store serves. - -Tax regions can inherit settings and rules from a parent tax region. - -*** - -## Tax Rules in a Tax Region - -Tax rules define the tax rates and behavior within a tax region. They specify: - -- The tax rate percentage. -- Which products the tax applies to. -- Other custom rules to determine tax applicability. - -Learn more about tax rules in the [Tax Rates and Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-rates-and-rules/index.html.md) guide. - -*** - -## Tax Provider - -Each tax region can have a default tax provider. The tax provider is responsible for calculating the tax lines for carts and orders in that region. - -You can use Medusa's default tax provider or create a custom one, allowing you to integrate with third-party tax services or implement your own tax calculation logic. - -Learn more about tax providers in the [Tax Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-provider/index.html.md) guide. - - # Tax Rates and Rules In this document, you’ll learn about tax rates and rules. @@ -30059,240 +30244,39 @@ These two properties of the data model identify the rule’s target: 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. -# Links between Store Module and Other Modules +# Tax Region -This document showcases the module links defined between the Store Module and other Commerce Modules. +In this document, you’ll learn about tax regions and how to use them with the Region Module. -## Summary +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. -The Store Module has the following links to other modules: +## What is a Tax Region? -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. +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. -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -|StoreCurrency|Currency|Read-only - has many|Learn more| +Tax regions can inherit settings and rules from a parent tax region. *** -## Currency Module +## Tax Rules in a Tax Region -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. +Tax rules define the tax rates and behavior within a tax region. They specify: -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). +- The tax rate percentage. +- Which products the tax applies to. +- Other custom rules to determine tax applicability. -### Retrieve with Query - -To retrieve the details of a store's currencies with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `supported_currencies.currency.*` in `fields`: - -### query.graph - -```ts -const { data: stores } = await query.graph({ - entity: "store", - fields: [ - "supported_currencies.currency.*", - ], -}) - -// stores[0].supported_currencies -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: stores } = useQueryGraphStep({ - entity: "store", - fields: [ - "supported_currencies.currency.*", - ], -}) - -// stores[0].supported_currencies -``` - - -# User Module Options - -In this document, you'll learn about the options of the User Module. - -## Module Options - -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" - -// ... - -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/user", - options: { - jwt_secret: process.env.JWT_SECRET, - }, - }, - ], -}) -``` - -|Option|Description|Required| -|---|---|---|---|---| -|\`jwt\_secret\`|A string indicating the secret used to sign the invite tokens.|Yes| - -### Environment Variables - -Make sure to add the necessary environment variables for the above options in `.env`: - -```bash -JWT_SECRET=supersecret -``` - - -# 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. +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. *** -## Register the Emailpass Auth Module Provider +## Tax Provider -The Emailpass auth provider is registered by default with the Auth Module. +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. -If you want to pass options to the provider, add the provider to the `providers` option of the Auth Module: +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. -```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) - - -# User Creation Flows - -In this document, learn the different ways to create a user using the User Module. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/users/index.html.md) to learn how to manage users using the dashboard. - -## Straightforward User Creation - -To create a user, use the [create method of the User Module’s main service](https://docs.medusajs.com/references/user/create/index.html.md): - -```ts -const user = await userModuleService.createUsers({ - email: "user@example.com", -}) -``` - -You can pair this with the Auth Module to allow the user to authenticate, as explained in a [later section](#create-identity-with-the-auth-module). - -*** - -## Invite Users - -To create a user, you can create an invite for them using the [createInvites method](https://docs.medusajs.com/references/user/createInvites/index.html.md) of the User Module's main service: - -```ts -const invite = await userModuleService.createInvites({ - email: "user@example.com", -}) -``` - -Later, you can accept the invite and create a new user for them: - -```ts -const invite = - await userModuleService.validateInviteToken("secret_123") - -await userModuleService.updateInvites({ - id: invite.id, - accepted: true, -}) - -const user = await userModuleService.createUsers({ - email: invite.email, -}) -``` - -### Invite Expiry - -An invite has an expiry date. You can renew the expiry date and refresh the token using the [refreshInviteTokens method](https://docs.medusajs.com/references/user/refreshInviteTokens/index.html.md): - -```ts -await userModuleService.refreshInviteTokens(["invite_123"]) -``` - -*** - -## Create Identity with the Auth Module - -By combining the User and Auth Modules, you can use the Auth Module for authenticating users, and the User Module to manage those users. - -So, when a user is authenticated, and you receive the `AuthIdentity` object, you can use it to create a user if it doesn’t exist: - -```ts -const { success, authIdentity } = - await authModuleService.authenticate("emailpass", { - // ... - }) - -const [, count] = await userModuleService.listAndCountUsers({ - email: authIdentity.entity_id, -}) - -if (!count) { - const user = await userModuleService.createUsers({ - email: authIdentity.entity_id, - }) -} -``` +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 @@ -30464,6 +30448,68 @@ The [Authenticate or Login API Route](https://docs.medusajs.com/Users/shahednass - [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) + + # Stripe Module Provider In this document, you’ll learn about the Stripe Module Provider and how to configure it in the Payment Module. @@ -31259,27 +31305,21 @@ This subscriber listens to the `order.placed` event and executes the `trackOrder You'll now track the order creation event whenever an order is placed in your Medusa application. You can test this out by placing an order and checking the provider you integrated with (for example, PostHog) for the tracked event. -# Cache Module +# File Module -In this document, you'll learn what a Cache Module is and how to use it in your Medusa application. +In this document, you'll learn about the File Module and its providers. -## What is a Cache Module? +## What is the File 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). +The File Module exposes the functionalities to upload assets, such as product images, to the Medusa application. Medusa uses the File Module in its core commerce features for all file operations, and you can use it in your custom features as well. *** -## How to Use the Cache Module? +## How to Use the File 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. +You can use the File 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. +In a step of your workflow, you can resolve the File Module's service and use its methods to upload files, retrieve files, or delete files. For example: @@ -31288,39 +31328,51 @@ import { Modules } from "@medusajs/framework/utils" import { createStep, createWorkflow, + StepResponse, + WorkflowResponse, } from "@medusajs/framework/workflows-sdk" const step1 = createStep( "step-1", async ({}, { container }) => { - const cacheModuleService = container.resolve( - Modules.CACHE + const fileModuleService = container.resolve( + Modules.FILE ) - await cacheModuleService.set("key", "value") + const { url } = await fileModuleService.retrieveFile("image.png") + + return new StepResponse(url) } ) export const workflow = createWorkflow( "workflow-1", () => { - step1() + const url = step1() + + return new WorkflowResponse(url) } ) ``` -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). +In the example above, you create a workflow that has a step. In the step, you resolve the service of the File 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"`. +Then, you use the `retrieveFile` method of the File Module to retrieve the URL of the file with the name `"image.png"`. The URL is then returned as a response from the step and the workflow. *** -## List of Cache Modules +### What is a File Module Provider? -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). +A File Module Provider implements the underlying logic of handling uploads and downloads of assets, such as integrating third-party services. The File Module then uses the registered File Module Provider to handle file operations. -- [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) +Only one File Module Provider can be registered at a time. If you register multiple providers, the File Module will throw an error. + +By default, Medusa uses the [Local File Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/file/local/index.html.md). This module uploads files to the `uploads` directory of your Medusa application. + +This is useful for development. However, for production, it’s highly recommended to use other File Module Providers, such as the S3 File Module Provider. You can also [Create a File Provider](https://docs.medusajs.com/references/file-provider-module/index.html.md). + +- [Local](https://docs.medusajs.com/infrastructure-modules/file/local/index.html.md) +- [AWS S3 (and Compatible APIs)](https://docs.medusajs.com/infrastructure-modules/file/s3/index.html.md) # Locking Module @@ -31436,255 +31488,6 @@ 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) -# 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, - }) - } -) - -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). - -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). - -*** - -## What is a Notification Module Provider? - -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. - -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). - -- [Local](https://docs.medusajs.com/infrastructure-modules/notification/local/index.html.md) -- [SendGrid](https://docs.medusajs.com/infrastructure-modules/notification/sendgrid/index.html.md) - -*** - -## Notification Module Provider Channels - -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. - -For example: - -```ts title="medusa-config.ts" highlights={[["19"]]} -import { Modules } from "@medusajs/framework/utils" - -// ... - -module.exports = { - // ... - modules: [ - // ... - { - resolve: "@medusajs/medusa/notification", - options: { - providers: [ - // ... - { - resolve: "@medusajs/medusa/notification-local", - id: "notification", - options: { - channels: ["email"], - }, - }, - ], - }, - }, - ], -} -``` - -The `channels` option is an array of strings indicating the channels this provider is used for. - - -# Workflow Engine Module - -In this document, you'll learn what a Workflow Engine Module is and how to use it in your Medusa application. - -## What is a Workflow Engine Module? - -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. - -*** - -## How to Use the Workflow Engine Module? - -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. - -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) - - -# File Module - -In this document, you'll learn about the File Module and its providers. - -## What is the File Module? - -The File Module exposes the functionalities to upload assets, such as product images, to the Medusa application. Medusa uses the File Module in its core commerce features for all file operations, and you can use it in your custom features as well. - -*** - -## How to Use the File Module? - -You can use the File 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 File Module's service and use its methods to upload files, retrieve files, or delete files. - -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 fileModuleService = container.resolve( - Modules.FILE - ) - - const { url } = await fileModuleService.retrieveFile("image.png") - - return new StepResponse(url) - } -) - -export const workflow = createWorkflow( - "workflow-1", - () => { - const url = step1() - - return new WorkflowResponse(url) - } -) -``` - -In the example above, you create a workflow that has a step. In the step, you resolve the service of the File Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). - -Then, you use the `retrieveFile` method of the File Module to retrieve the URL of the file with the name `"image.png"`. The URL is then returned as a response from the step and the workflow. - -*** - -### What is a File Module Provider? - -A File Module Provider implements the underlying logic of handling uploads and downloads of assets, such as integrating third-party services. The File Module then uses the registered File Module Provider to handle file operations. - -Only one File Module Provider can be registered at a time. If you register multiple providers, the File Module will throw an error. - -By default, Medusa uses the [Local File Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/file/local/index.html.md). This module uploads files to the `uploads` directory of your Medusa application. - -This is useful for development. However, for production, it’s highly recommended to use other File Module Providers, such as the S3 File Module Provider. You can also [Create a File Provider](https://docs.medusajs.com/references/file-provider-module/index.html.md). - -- [Local](https://docs.medusajs.com/infrastructure-modules/file/local/index.html.md) -- [AWS S3 (and Compatible APIs)](https://docs.medusajs.com/infrastructure-modules/file/s3/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. @@ -31757,6 +31560,208 @@ Medusa provides the following Event Modules. You can use one of them, or [Create - [Redis](https://docs.medusajs.com/infrastructure-modules/event/redis/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) + + +# Local Analytics Module Provider + +The Local Analytics Module Provider is a simple analytics provider for Medusa that logs analytics events to the console. It's useful for development and debugging purposes. + +The Analytics Module and its providers are available starting [Medusa v2.8.3](https://github.com/medusajs/medusa/releases/tag/v2.8.3). + +*** + +## Register the Local Analytics Module + +Add the module into the `provider` object of the Analytics Module: + +You can use only one Analytics Module Provider in your Medusa application. + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/analytics", + options: { + provider: { + resolve: "@medusajs/analytics-local", + id: "local", + }, + }, + }, + ], +}) +``` + +*** + +## Test out the Module + +To test the module out, you'll track in the console when an order is placed. + +You'll first create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) that tracks the order completion event. Then, you can execute the workflow in a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) that listens to the `order.placed` event. + +For example, create a workflow at `src/workflows/track-order-placed.ts` with the following content: + +```ts title="src/workflows/track-order-created.ts" highlights={workflowHighlights} +import { createWorkflow } from "@medusajs/framework/workflows-sdk" +import { createStep } from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" +import { OrderDTO } from "@medusajs/framework/types" + +type StepInput = { + order: OrderDTO +} + +const trackOrderCreatedStep = createStep( + "track-order-created-step", + async ({ order }: StepInput, { container }) => { + const analyticsModuleService = container.resolve(Modules.ANALYTICS) + + await analyticsModuleService.track({ + event: "order_created", + userId: order.customer_id, + properties: { + order_id: order.id, + total: order.total, + items: order.items.map((item) => ({ + variant_id: item.variant_id, + product_id: item.product_id, + quantity: item.quantity, + })), + customer_id: order.customer_id, + }, + }) + } +) + +type WorkflowInput = { + order_id: string +} + +export const trackOrderCreatedWorkflow = createWorkflow( + "track-order-created-workflow", + ({ order_id }: WorkflowInput) => { + const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "*", + "customer.*", + "items.*" + ], + filters: { + id: order_id, + }, + }) + trackOrderCreatedStep({ + order: orders[0], + }) + } +) +``` + +This workflow retrieves the order details using the `useQueryGraphStep` and then tracks the order creation event using the `trackOrderCreatedStep`. + +In the step, you resolve the service of the Analytics Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) and use its `track` method to track the event. This method will use the underlying provider configured (which is the Local Analytics Module Provider, in this case) to track the event. + +Next, create a subscriber at `src/subscribers/order-placed.ts` with the following content: + +```ts title="src/subscribers/order-placed.ts" +import type { + SubscriberArgs, + SubscriberConfig, +} from "@medusajs/framework" +import { trackOrderCreatedWorkflow } from "../workflows/track-order-created" + +export default async function orderPlacedHandler({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + await trackOrderCreatedWorkflow(container).run({ + input: { + order_id: data.id, + }, + }) +} + +export const config: SubscriberConfig = { + event: "order.placed", +} +``` + +This subscriber listens to the `order.placed` event and executes the `trackOrderCreatedWorkflow` workflow, passing the order ID as input. + +You'll now track the order creation event whenever an order is placed in your Medusa application. You can test this out by placing an order and checking the console for the tracked event. + +*** + +## Additional Resources + +- [How to Use the Analytics Module](https://docs.medusajs.com/references/analytics/service/index.html.md) + + # PostHog Analytics Module Provider The PostHog Analytics Module Provider allows you to integrate [PostHog](https://posthog.com/) with Medusa. @@ -31924,182 +31929,953 @@ You'll now track the order creation event whenever an order is placed in your Me - [How to Use the Analytics Module](https://docs.medusajs.com/references/analytics/service/index.html.md) -# Local Analytics Module Provider +# Local File Module Provider -The Local Analytics Module Provider is a simple analytics provider for Medusa that logs analytics events to the console. It's useful for development and debugging purposes. +The Local File Module Provider stores files uploaded to your Medusa application in the `/uploads` directory. -The Analytics Module and its providers are available starting [Medusa v2.8.3](https://github.com/medusajs/medusa/releases/tag/v2.8.3). +- The Local File Module Provider is only for development purposes. Use the [S3 File Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/file/s3/index.html.md) in production instead. +- The Local File Module Provider will only read files uploaded through Medusa. It will not read files uploaded manually to the `static` (or other configured) directory. *** -## Register the Local Analytics Module +## Register the Local File Module -Add the module into the `provider` object of the Analytics Module: +The Local File Module Provider is registered by default in your application. -You can use only one Analytics Module Provider in your Medusa application. +Add the module into the `providers` array of the File Module: + +The File Module accepts one provider only. + +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" + +// ... + +module.exports = { + // ... + modules: [ + { + resolve: "@medusajs/medusa/file", + options: { + providers: [ + { + resolve: "@medusajs/medusa/file-local", + id: "local", + options: { + // provider options... + }, + }, + ], + }, + }, + ], +} +``` + +### Local File Module Options + +|Option|Description|Default| +|---|---|---|---|---| +|\`upload\_dir\`|The directory to upload files to. Medusa exposes the content of the |\`static\`| +|\`backend\_url\`|The URL that serves the files.|\`http://localhost:9000/static\`| + + +# S3 File Module Provider + +The S3 File Module Provider integrates Amazon S3 and services following a compatible API (such as MinIO or DigitalOcean Spaces) to store files uploaded to your Medusa application. + +Medusa Cloud offers a managed file storage solution with AWS S3 for your Medusa application. Contact the [sales team](https://medusajs.com/pricing/) to learn more. + +## Prerequisites + +### AWS S3 + +- [AWS account](https://console.aws.amazon.com/console/home?nc2=h_ct\&src=header-signin). +- Create [AWS user with AmazonS3FullAccess permissions](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-create-and-attach-iam-policy.html). +- Create [AWS user access key ID and secret access key](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html#Using_CreateAccessKey). +- Create [S3 bucket](https://docs.aws.amazon.com/AmazonS3/latest/userguide/create-bucket-overview.html) with the "Public Access setting" enabled: + 1. On your bucket's dashboard, click on the Permissions tab. + 2. Click on the Edit button of the Block public access (bucket settings) section. + 3. In the form that opens, don't toggle any checkboxes and click the "Save changes" button. + 4. Confirm saving the changes by entering `confirm` in the pop-up that shows. + 5. Back on the Permissions page, scroll to the Object Ownership section and click the Edit button. + 6. In the form that opens: + - Choose the "ACLs enabled" card. + - Click on the "Save changes" button. + 7. Back on the Permissions page, scroll to the "Access Control List (ACL)" section and click on the Edit button. + 8. In the form that opens, enable the Read permission for "Everyone (public access)". + 9. Check the "I understand the effects of these changes on my objects and buckets." checkbox. + 10. Click on the "Save changes" button. + +### MinIO + +- Create [DigitalOcean account](https://cloud.digitalocean.com/registrations/new). +- Create [DigitalOcean Spaces bucket](https://docs.digitalocean.com/products/spaces/how-to/create/). +- Create [DigitalOcean Spaces access and secret access keys](https://docs.digitalocean.com/products/spaces/how-to/manage-access/#access-keys). + +### DigitalOcean Spaces + +1. Create a [Cloudflare account](https://dash.cloudflare.com/sign-up). +2. Set up your R2 bucket: + - Navigate to R2 Object Storage in your dashboard. You may need to provide your credit-card information. + - Click "Create bucket" + - Enter a unique bucket name + - Select "Automatic" for location + - Choose "Standard" for storage class + - Confirm by clicking "Create bucket" +3. Configure public access: + - On your bucket's dashboard, click on the Settings tab. + - Scroll down to the Public Access section, and click on "Allow Access" in the "R2.dev subdomain" card. + - Type 'allow' to confirm + - Copy the Public R2.dev Bucket URL for your `S3_FILE_URL` +4. Retrieve credentials: + - [Go to API tokens page](https://dash.cloudflare.com/?to=/:account/r2/api-tokens): + - Select "Create API token" + - Edit the "R2 Token" name + - Under Permissions, select Object Read & Write permission types + - You can optionally specify the buckets that this API token has access to under the "Specify bucket(s)" section. + - Once done, click the "Create API Token" button. + - You'll receive an access key ID and a secret access key. Save them to use them later for the `S3_ACCESS_KEY_ID` and `S3_SECRET_ACCESS_KEY` environment variables. + +### Supabase S3 Storage + +### Cloudflare R2 + +*** + +## Register the S3 File Module + +Add the module into the `providers` array of the File Module: + +The File Module accepts one provider only. + +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" + +// ... + +module.exports = { + // ... + modules: [ + // ... + { + resolve: "@medusajs/medusa/file", + options: { + providers: [ + { + resolve: "@medusajs/medusa/file-s3", + id: "s3", + options: { + file_url: process.env.S3_FILE_URL, + access_key_id: process.env.S3_ACCESS_KEY_ID, + secret_access_key: process.env.S3_SECRET_ACCESS_KEY, + region: process.env.S3_REGION, + bucket: process.env.S3_BUCKET, + endpoint: process.env.S3_ENDPOINT, + // other options... + }, + }, + ], + }, + }, + ], +} +``` + +### Additional Configuration for MinIO and Supabase + +If you're using MinIO or Supabase, set `forcePathStyle` to `true` in the `additional_client_config` object. + +For example: ```ts title="medusa-config.ts" module.exports = defineConfig({ // ... modules: [ { - resolve: "@medusajs/medusa/analytics", + resolve: "@medusajs/medusa/file", options: { - provider: { - resolve: "@medusajs/analytics-local", - id: "local", - }, + providers: [ + { + resolve: "@medusajs/medusa/file-s3", + id: "s3", + options: { + // ... + additional_client_config: { + forcePathStyle: true, + }, + }, + }, + ], }, }, ], }) ``` +### S3 File Module Options + +|Option|Description|Default| +|---|---|---|---|---| +|\`file\_url\`|The base URL to upload files to.|-| +|\`access\_key\_id\`|The AWS or (S3 compatible) user's access key ID.|-| +|\`secret\_access\_key\`|The AWS or (S3 compatible) user's secret access key.|-| +|\`region\`|The bucket's region code.|-| +|\`bucket\`|The bucket's name.|-| +|\`endpoint\`|The URL to the AWS S3 (or compatible S3 API) server.|-| +|\`prefix\`|A string to prefix each uploaded file's name.|-| +|\`cache\_control\`|A string indicating how long objects remain in the AWS S3 (or compatible S3 API) cache.|\`public, max-age=31536000\`| +|\`download\_file\_duration\`|A number indicating the expiry time of presigned URLs in seconds.|\`3600\`| +|\`additional\_client\_config\`|Any additional configurations to pass to the S3 client.|-| + +*** + +## Troubleshooting + + +# Workflow Engine Module + +In this document, you'll learn what a Workflow Engine Module is and how to use it in your Medusa application. + +## What is a Workflow Engine Module? + +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. + +*** + +## How to Use the Workflow Engine Module? + +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. + +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: + +```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, + }) + } +) + +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). + +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). + +*** + +## What is a Notification Module Provider? + +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. + +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). + +- [Local](https://docs.medusajs.com/infrastructure-modules/notification/local/index.html.md) +- [SendGrid](https://docs.medusajs.com/infrastructure-modules/notification/sendgrid/index.html.md) + +*** + +## Notification Module Provider Channels + +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. + +For example: + +```ts title="medusa-config.ts" highlights={[["19"]]} +import { Modules } from "@medusajs/framework/utils" + +// ... + +module.exports = { + // ... + modules: [ + // ... + { + resolve: "@medusajs/medusa/notification", + options: { + providers: [ + // ... + { + resolve: "@medusajs/medusa/notification-local", + id: "notification", + options: { + channels: ["email"], + }, + }, + ], + }, + }, + ], +} +``` + +The `channels` option is an array of strings indicating the channels this provider is used for. + + +# 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 the module out, you'll track in the console when an order is placed. +To test out the Redis Locking Module Provider, start the Medusa application: -You'll first create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) that tracks the order completion event. Then, you can execute the workflow in a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) that listens to the `order.placed` event. - -For example, create a workflow at `src/workflows/track-order-placed.ts` with the following content: - -```ts title="src/workflows/track-order-created.ts" highlights={workflowHighlights} -import { createWorkflow } from "@medusajs/framework/workflows-sdk" -import { createStep } from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" -import { OrderDTO } from "@medusajs/framework/types" - -type StepInput = { - order: OrderDTO -} - -const trackOrderCreatedStep = createStep( - "track-order-created-step", - async ({ order }: StepInput, { container }) => { - const analyticsModuleService = container.resolve(Modules.ANALYTICS) - - await analyticsModuleService.track({ - event: "order_created", - userId: order.customer_id, - properties: { - order_id: order.id, - total: order.total, - items: order.items.map((item) => ({ - variant_id: item.variant_id, - product_id: item.product_id, - quantity: item.quantity, - })), - customer_id: order.customer_id, - }, - }) - } -) - -type WorkflowInput = { - order_id: string -} - -export const trackOrderCreatedWorkflow = createWorkflow( - "track-order-created-workflow", - ({ order_id }: WorkflowInput) => { - const { data: orders } = useQueryGraphStep({ - entity: "order", - fields: [ - "*", - "customer.*", - "items.*" - ], - filters: { - id: order_id, - }, - }) - trackOrderCreatedStep({ - order: orders[0], - }) - } -) +```bash npm2yarn +npm run dev ``` -This workflow retrieves the order details using the `useQueryGraphStep` and then tracks the order creation event using the `trackOrderCreatedStep`. +You'll see the following message logged in the terminal: -In the step, you resolve the service of the Analytics Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) and use its `track` method to track the event. This method will use the underlying provider configured (which is the Local Analytics Module Provider, in this case) to track the event. +```bash +info: Connection to Redis in "locking-redis" provider established +``` -Next, create a subscriber at `src/subscribers/order-placed.ts` with the following content: +This message indicates that the Redis Locking Module Provider has successfully connected to the Redis server. -```ts title="src/subscribers/order-placed.ts" -import type { - SubscriberArgs, - SubscriberConfig, -} from "@medusajs/framework" -import { trackOrderCreatedWorkflow } from "../workflows/track-order-created" +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. -export default async function orderPlacedHandler({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - await trackOrderCreatedWorkflow(container).run({ - input: { - order_id: data.id, +*** + +## 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", + }) + } +) +``` + + +# How to Create an Event Module + +In this guide, you’ll learn how to create an Event Module. + +## 1. Create Module Directory + +Start by creating a new directory for your module. For example, `src/modules/my-event`. + +*** + +## 2. Create the Event Service + +Create the file `src/modules/my-event/service.ts` that holds the implementation of the event service. + +The Event Module's main service must extend the `AbstractEventBusModuleService` class from the Medusa Framework: + +```ts title="src/modules/my-event/service.ts" +import { AbstractEventBusModuleService } from "@medusajs/framework/utils" +import { Message } from "@medusajs/types" + +class MyEventService extends AbstractEventBusModuleService { + async emit(data: Message | Message[], options: Record): Promise { + throw new Error("Method not implemented.") + } + async releaseGroupedEvents(eventGroupId: string): Promise { + throw new Error("Method not implemented.") + } + async clearGroupedEvents(eventGroupId: string): Promise { + throw new Error("Method not implemented.") + } } -export const config: SubscriberConfig = { - event: "order.placed", +export default MyEventService +``` + +The service implements the required methods based on the desired publish/subscribe logic. + +### eventToSubscribersMap\_ Property + +The `AbstractEventBusModuleService` has a field `eventToSubscribersMap_`, which is a JavaScript Map. The map's keys are the event names, whereas the value of each key is an array of subscribed handler functions. + +In your custom implementation, you can use this property to manage the subscribed handler functions: + +```ts +const eventSubscribers = + this.eventToSubscribersMap_.get(eventName) || [] +``` + +### emit Method + +The `emit` method is used to push an event from the Medusa application into your messaging system. The subscribers to that event would then pick up the message and execute their asynchronous tasks. + +An example implementation: + +```ts title="src/modules/my-event/service.ts" +class MyEventService extends AbstractEventBusModuleService { + async emit(data: Message | Message[], options: Record): Promise { + const events = Array.isArray(data) ? data : [data] + + for (const event of events) { + console.log(`Received the event ${event.name} with data ${event.data}`) + + // TODO push the event somewhere + } + } + // ... } ``` -This subscriber listens to the `order.placed` event and executes the `trackOrderCreatedWorkflow` workflow, passing the order ID as input. +The `emit` method receives the following parameters: -You'll now track the order creation event whenever an order is placed in your Medusa application. You can test this out by placing an order and checking the console for the tracked event. +- data: (\`object or array of objects\`) The emitted event(s). + + - name: (\`string\`) The name of the emitted event. + + - data: (\`object\`) The data payload of the event. + + - metadata: (\`object\`) Additional details of the emitted event. + + - eventGroupId: (string) A group ID that the event belongs to. + + - options: (\`object\`) Additional options relevant for the event service. + +### releaseGroupedEvents Method + +Grouped events are useful when you have distributed transactions where you need to explicitly group, release, and clear events upon lifecycle transaction events. + +If your Event Module supports grouped events, this method is used to emit all events in a group, then clear that group. + +For example: + +```ts title="src/modules/my-event/service.ts" +class MyEventService extends AbstractEventBusModuleService { + protected groupedEventsMap_: Map + + constructor() { + // @ts-ignore + super(...arguments) + + this.groupedEventsMap_ = new Map() + } + + async releaseGroupedEvents(eventGroupId: string): Promise { + const groupedEvents = this.groupedEventsMap_.get(eventGroupId) || [] + + for (const event of groupedEvents) { + const { options, ...eventBody } = event + + // TODO emit event + } + + await this.clearGroupedEvents(eventGroupId) + } + + // ... +} +``` + +The `releaseGroupedEvents` receives the group ID as a parameter. + +In the example above, you add a `groupedEventsMap_` property to store grouped events. Then, in the method, you emit the events in the group, then clear the grouped events using the `clearGroupedEvents` which you'll learn about next. + +To add events to the grouped events map, you can do it in the `emit` method: + +```ts title="src/modules/my-event/service.ts" +class MyEventService extends AbstractEventBusModuleService { + // ... + async emit(data: Message | Message[], options: Record): Promise { + const events = Array.isArray(data) ? data : [data] + + for (const event of events) { + console.log(`Received the event ${event.name} with data ${event.data}`) + + if (event.metadata.eventGroupId) { + const groupedEvents = this.groupedEventsMap_.get( + event.metadata.eventGroupId + ) || [] + + groupedEvents.push(event) + + this.groupedEventsMap_.set(event.metadata.eventGroupId, groupedEvents) + continue + } + + // TODO push the event somewhere + } + } +} +``` + +### clearGroupedEvents Method + +If your Event Module supports grouped events, this method is used to remove the events of a group. + +For example: + +```ts title="src/modules/my-event/service.ts" +class MyEventService extends AbstractEventBusModuleService { + // from previous section + protected groupedEventsMap_: Map + + async clearGroupedEvents(eventGroupId: string): Promise { + this.groupedEventsMap_.delete(eventGroupId) + } + + // ... +} +``` + +The method accepts the group's name as a parameter. + +In the method, you delete the group from the `groupedEventsMap_` property (added in the previous section), deleting the stored events of it as well. *** -## Additional Resources +## 3. Create Module Definition File -- [How to Use the Analytics Module](https://docs.medusajs.com/references/analytics/service/index.html.md) +Create the file `src/modules/my-event/index.ts` with the following content: +```ts title="src/modules/my-event/index.ts" +import MyEventService from "./service" +import { Module } from "@medusajs/framework/utils" -# In-Memory Cache Module +export default Module("my-event", { + service: MyEventService, +}) +``` -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). +This exports the module's definition, indicating that the `MyEventService` is the main service of the module. *** -## Register the In-Memory Cache Module +## 4. Use Module -The In-Memory Cache Module is registered by default in your application. +To use your Event Module, add it to the `modules` object exported as part of the configurations in `medusa-config.ts`. An Event Module is added under the `eventBus` key. -Add the module into the `modules` property of the exported object in `medusa-config.ts`: +For example: ```ts title="medusa-config.ts" import { Modules } from "@medusajs/framework/utils" + // ... module.exports = defineConfig({ // ... modules: [ { - resolve: "@medusajs/medusa/cache-inmemory", - options: { - // optional options + resolve: "./src/modules/my-event", + options: { + // any 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\`| +# Local Event Module + +The Local Event Module uses Node EventEmitter to implement Medusa's pub/sub events system. The Node EventEmitter is limited to a single process environment. + +This module is useful for development and testing, but it’s not recommended to be used in production. + +For production, it’s recommended to use modules like [Redis Event Bus Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/redis/index.html.md). + +*** + +## Register the Local Event Module + +The Local Event 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/event-bus-local", + }, + ], +}) +``` + +*** + +## 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 +Local Event Bus installed. This is not recommended for production. +``` + + +# Redis Event Module + +The Redis Event Module uses Redis to implement Medusa's pub/sub events system. + +It's powered by BullMQ and `io-redis`. BullMQ is responsible for the message queue and worker, and `io-redis` is the underlying Redis client that BullMQ connects to for events storage. + +In production, it's recommended to use this module. + +*** + +## Register the Redis Event 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" +import { Modules } from "@medusajs/framework/utils" + +// ... + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/event-bus-redis", + options: { + redisUrl: process.env.EVENTS_REDIS_URL, + }, + }, + ], +}) +``` + +### Environment Variables + +Make sure to add the following environment variables: + +```bash +EVENTS_REDIS_URL= +``` + +### Redis Event 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|-| +|\`queueName\`|A string indicating BullMQ's queue name.|No|\`events-queue\`| +|\`queueOptions\`|An object of options to pass to the BullMQ constructor. Refer to |No|-| +|\`workerOptions\`|An object of options to pass to the BullMQ Worker constructor. Refer to |No|-| +|\`jobOptions\`|An object of options to pass to jobs added to the BullMQ queue. Refer to |No|-| + +## 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 'event-redis' established +``` # How to Create a Cache Module @@ -32272,81 +33048,15 @@ module.exports = defineConfig({ ``` -# PostgreSQL Locking Module Provider +# How to Use the Workflow Engine Module -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. +In this document, you’ll learn about the different methods in the Workflow Engine Module's service and how to use them. *** -## Register the PostgreSQL Locking Module Provider +## Resolve Workflow Engine Module's Service -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): +In your workflow's step, you can resolve the Workflow Engine Module's service from the Medusa container: ```ts import { Modules } from "@medusajs/framework/utils" @@ -32355,17 +33065,177 @@ import { createStep } from "@medusajs/framework/workflows-sdk" const step1 = createStep( "step-1", async ({}, { container }) => { - const lockingModuleService = container.resolve( - Modules.LOCKING + const workflowEngineModuleService = container.resolve( + Modules.WORKFLOW_ENGINE ) - - await lockingModuleService.acquire("prod_123", { - provider: "lp_locking-postgres", - }) + + // TODO use workflowEngineModuleService } ) ``` +This will resolve the service of the configured Workflow Engine Module, which is 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. + +You can then use the Workflow Engine Module's service's methods in the step. The rest of this guide details these methods. + +*** + +## setStepSuccess + +This method sets an async step in a currently-executing [long-running workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/long-running-workflow/index.html.md) as successful. The workflow will then continue to the next step. + +### Example + +```ts +// other imports... +import { + TransactionHandlerType, +} from "@medusajs/framework/utils" + +await workflowEngineModuleService.setStepSuccess({ + idempotencyKey: { + action: TransactionHandlerType.INVOKE, + transactionId, + stepId: "step-2", + workflowId: "hello-world", + }, + stepResponse: new StepResponse("Done!"), + options: { + container, + }, +}) +``` + +### Parameters + +- idempotencyKey: (\`object\`) The details of the step to set as successful. + + - action: (\`invoke\` | \`compensate\`) If the step's compensation function is running, use \`compensate\`. Otherwise, use \`invoke\`. + + - transactionId: (\`string\`) The ID of the workflow execution's transaction. + + - stepId: (\`string\`) The ID of the step to change its status. This is the first parameter passed to \`createStep\` when creating the step. + + - workflowId: (\`string\`) The ID of the workflow. This is the first parameter passed to \`createWorkflow\` when creating the workflow. +- stepResponse: (\`StepResponse\`) Set the response of the step. This is similar to the response you return in a step's definition, but since the async step doesn't have a response, you set its response when changing its status. +- options: (\`object\`) Options to pass to the step. + + - container: (\`Container\`) An instance of the Medusa container. + +*** + +## setStepFailure + +This method sets an async step in a currently-executing [long-running workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/long-running-workflow/index.html.md) as failed. The workflow will then stop executing and the compensation functions of the workflow's steps will be executed. + +### Example + +```ts +// other imports... +import { + TransactionHandlerType, +} from "@medusajs/framework/utils" + +await workflowEngineModuleService.setStepFailure({ + idempotencyKey: { + action: TransactionHandlerType.INVOKE, + transactionId, + stepId: "step-2", + workflowId: "hello-world", + }, + stepResponse: new StepResponse("Failed!"), + options: { + container, + }, +}) +``` + +### Parameters + +- idempotencyKey: (\`object\`) The details of the step to set as failed. + + - action: (\`invoke\` | \`compensate\`) If the step's compensation function is running, use \`compensate\`. Otherwise, use \`invoke\`. + + - transactionId: (\`string\`) The ID of the workflow execution's transaction. + + - stepId: (\`string\`) The ID of the step to change its status. This is the first parameter passed to \`createStep\` when creating the step. + + - workflowId: (\`string\`) The ID of the workflow. This is the first parameter passed to \`createWorkflow\` when creating the workflow. +- stepResponse: (\`StepResponse\`) Set the response of the step. This is similar to the response you return in a step's definition, but since the async step doesn't have a response, you set its response when changing its status. +- options: (\`object\`) Options to pass to the step. + + - container: (\`Container\`) An instance of the Medusa container. + +*** + +## subscribe + +This method subscribes to a workflow's events. You can use this method to listen to a [long-running workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/long-running-workflow/index.html.md)'s events and retrieve its result once it's done executing. + +Refer to the [Long-Running Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/long-running-workflow#access-long-running-workflow-status-and-result/index.html.md) documentation to learn more. + +### Example + +```ts +const { transaction } = await helloWorldWorkflow(container).run() + +const subscriptionOptions = { + workflowId: "hello-world", + transactionId: transaction.transactionId, + subscriberId: "hello-world-subscriber", +} + +await workflowEngineModuleService.subscribe({ + ...subscriptionOptions, + subscriber: async (data) => { + if (data.eventType === "onFinish") { + console.log("Finished execution", data.result) + // unsubscribe + await workflowEngineModuleService.unsubscribe({ + ...subscriptionOptions, + subscriberOrId: subscriptionOptions.subscriberId, + }) + } else if (data.eventType === "onStepFailure") { + console.log("Workflow failed", data.step) + } + }, +}) +``` + +### Parameters + +- subscriptionOptions: (\`object\`) The options for the subscription. + + - workflowId: (\`string\`) The ID of the workflow to subscribe to. This is the first parameter passed to \`createWorkflow\` when creating the workflow. + + - transactionId: (\`string\`) The ID of the workflow execution's transaction. This is returned when you execute a workflow. + + - subscriberId: (\`string\`) A unique ID for the subscriber. It's used to unsubscribe from the workflow's events. + + - subscriber: (\`(data: WorkflowEvent) => void\`) The subscriber function that will be called when the workflow emits an event. + +*** + +## unsubscribe + +This method unsubscribes from a workflow's events. You can use this method to stop listening to a [long-running workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/long-running-workflow/index.html.md)'s events after you've received the result. + +### Example + +```ts +await workflowEngineModuleService.unsubscribe({ + workflowId: "hello-world", + transactionId: "transaction-id", + subscriberOrId: "hello-world-subscriber", +}) +``` + +### Parameters + +- workflowId: (\`string\`) The ID of the workflow to unsubscribe from. This is the first parameter passed to \`createWorkflow\` when creating the workflow. +- transactionId: (\`string\`) The ID of the workflow execution's transaction. This is returned when you execute a workflow. +- subscriberOrId: (\`string\`) The subscriber ID or the subscriber function to unsubscribe from the workflow's events. + # Redis Cache Module @@ -32433,6 +33303,144 @@ 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. @@ -32482,142 +33490,6 @@ module.exports = defineConfig({ it's important to specify its channels to make sure it's used when a notification for that channel is created.| -# 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", - }) - } -) -``` - - # SendGrid Notification Module Provider The SendGrid Notification Module Provider integrates [SendGrid](https://sendgrid.com) to send emails to users and customers. @@ -32955,1164 +33827,339 @@ export const config: SubscriberConfig = { ``` -# 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. - -*** - -## Resolve Workflow Engine Module's Service - -In your workflow's step, you can resolve the Workflow Engine Module's service from the Medusa container: - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createStep } from "@medusajs/framework/workflows-sdk" - -const step1 = createStep( - "step-1", - async ({}, { container }) => { - const workflowEngineModuleService = container.resolve( - Modules.WORKFLOW_ENGINE - ) - - // TODO use workflowEngineModuleService - } -) -``` - -This will resolve the service of the configured Workflow Engine Module, which is 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. - -You can then use the Workflow Engine Module's service's methods in the step. The rest of this guide details these methods. - -*** - -## setStepSuccess - -This method sets an async step in a currently-executing [long-running workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/long-running-workflow/index.html.md) as successful. The workflow will then continue to the next step. - -### Example - -```ts -// other imports... -import { - TransactionHandlerType, -} from "@medusajs/framework/utils" - -await workflowEngineModuleService.setStepSuccess({ - idempotencyKey: { - action: TransactionHandlerType.INVOKE, - transactionId, - stepId: "step-2", - workflowId: "hello-world", - }, - stepResponse: new StepResponse("Done!"), - options: { - container, - }, -}) -``` - -### Parameters - -- idempotencyKey: (\`object\`) The details of the step to set as successful. - - - action: (\`invoke\` | \`compensate\`) If the step's compensation function is running, use \`compensate\`. Otherwise, use \`invoke\`. - - - transactionId: (\`string\`) The ID of the workflow execution's transaction. - - - stepId: (\`string\`) The ID of the step to change its status. This is the first parameter passed to \`createStep\` when creating the step. - - - workflowId: (\`string\`) The ID of the workflow. This is the first parameter passed to \`createWorkflow\` when creating the workflow. -- stepResponse: (\`StepResponse\`) Set the response of the step. This is similar to the response you return in a step's definition, but since the async step doesn't have a response, you set its response when changing its status. -- options: (\`object\`) Options to pass to the step. - - - container: (\`Container\`) An instance of the Medusa container. - -*** - -## setStepFailure - -This method sets an async step in a currently-executing [long-running workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/long-running-workflow/index.html.md) as failed. The workflow will then stop executing and the compensation functions of the workflow's steps will be executed. - -### Example - -```ts -// other imports... -import { - TransactionHandlerType, -} from "@medusajs/framework/utils" - -await workflowEngineModuleService.setStepFailure({ - idempotencyKey: { - action: TransactionHandlerType.INVOKE, - transactionId, - stepId: "step-2", - workflowId: "hello-world", - }, - stepResponse: new StepResponse("Failed!"), - options: { - container, - }, -}) -``` - -### Parameters - -- idempotencyKey: (\`object\`) The details of the step to set as failed. - - - action: (\`invoke\` | \`compensate\`) If the step's compensation function is running, use \`compensate\`. Otherwise, use \`invoke\`. - - - transactionId: (\`string\`) The ID of the workflow execution's transaction. - - - stepId: (\`string\`) The ID of the step to change its status. This is the first parameter passed to \`createStep\` when creating the step. - - - workflowId: (\`string\`) The ID of the workflow. This is the first parameter passed to \`createWorkflow\` when creating the workflow. -- stepResponse: (\`StepResponse\`) Set the response of the step. This is similar to the response you return in a step's definition, but since the async step doesn't have a response, you set its response when changing its status. -- options: (\`object\`) Options to pass to the step. - - - container: (\`Container\`) An instance of the Medusa container. - -*** - -## subscribe - -This method subscribes to a workflow's events. You can use this method to listen to a [long-running workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/long-running-workflow/index.html.md)'s events and retrieve its result once it's done executing. - -Refer to the [Long-Running Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/long-running-workflow#access-long-running-workflow-status-and-result/index.html.md) documentation to learn more. - -### Example - -```ts -const { transaction } = await helloWorldWorkflow(container).run() - -const subscriptionOptions = { - workflowId: "hello-world", - transactionId: transaction.transactionId, - subscriberId: "hello-world-subscriber", -} - -await workflowEngineModuleService.subscribe({ - ...subscriptionOptions, - subscriber: async (data) => { - if (data.eventType === "onFinish") { - console.log("Finished execution", data.result) - // unsubscribe - await workflowEngineModuleService.unsubscribe({ - ...subscriptionOptions, - subscriberOrId: subscriptionOptions.subscriberId, - }) - } else if (data.eventType === "onStepFailure") { - console.log("Workflow failed", data.step) - } - }, -}) -``` - -### Parameters - -- subscriptionOptions: (\`object\`) The options for the subscription. - - - workflowId: (\`string\`) The ID of the workflow to subscribe to. This is the first parameter passed to \`createWorkflow\` when creating the workflow. - - - transactionId: (\`string\`) The ID of the workflow execution's transaction. This is returned when you execute a workflow. - - - subscriberId: (\`string\`) A unique ID for the subscriber. It's used to unsubscribe from the workflow's events. - - - subscriber: (\`(data: WorkflowEvent) => void\`) The subscriber function that will be called when the workflow emits an event. - -*** - -## unsubscribe - -This method unsubscribes from a workflow's events. You can use this method to stop listening to a [long-running workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/long-running-workflow/index.html.md)'s events after you've received the result. - -### Example - -```ts -await workflowEngineModuleService.unsubscribe({ - workflowId: "hello-world", - transactionId: "transaction-id", - subscriberOrId: "hello-world-subscriber", -}) -``` - -### Parameters - -- workflowId: (\`string\`) The ID of the workflow to unsubscribe from. This is the first parameter passed to \`createWorkflow\` when creating the workflow. -- transactionId: (\`string\`) The ID of the workflow execution's transaction. This is returned when you execute a workflow. -- subscriberOrId: (\`string\`) The subscriber ID or the subscriber function to unsubscribe from the workflow's events. - - -# Local File Module Provider - -The Local File Module Provider stores files uploaded to your Medusa application in the `/uploads` directory. - -- The Local File Module Provider is only for development purposes. Use the [S3 File Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/file/s3/index.html.md) in production instead. -- The Local File Module Provider will only read files uploaded through Medusa. It will not read files uploaded manually to the `static` (or other configured) directory. - -*** - -## Register the Local File Module - -The Local File Module Provider is registered by default in your application. - -Add the module into the `providers` array of the File Module: - -The File Module accepts one provider only. - -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" - -// ... - -module.exports = { - // ... - modules: [ - { - resolve: "@medusajs/medusa/file", - options: { - providers: [ - { - resolve: "@medusajs/medusa/file-local", - id: "local", - options: { - // provider options... - }, - }, - ], - }, - }, - ], -} -``` - -### Local File Module Options - -|Option|Description|Default| -|---|---|---|---|---| -|\`upload\_dir\`|The directory to upload files to. Medusa exposes the content of the |\`static\`| -|\`backend\_url\`|The URL that serves the files.|\`http://localhost:9000/static\`| - - -# 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 -``` - - -# 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", - }, - ], -}) -``` - - -# S3 File Module Provider - -The S3 File Module Provider integrates Amazon S3 and services following a compatible API (such as MinIO or DigitalOcean Spaces) to store files uploaded to your Medusa application. - -Medusa Cloud offers a managed file storage solution with AWS S3 for your Medusa application. Contact the [sales team](https://medusajs.com/pricing/) to learn more. - -## Prerequisites - -### AWS S3 - -- [AWS account](https://console.aws.amazon.com/console/home?nc2=h_ct\&src=header-signin). -- Create [AWS user with AmazonS3FullAccess permissions](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-create-and-attach-iam-policy.html). -- Create [AWS user access key ID and secret access key](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html#Using_CreateAccessKey). -- Create [S3 bucket](https://docs.aws.amazon.com/AmazonS3/latest/userguide/create-bucket-overview.html) with the "Public Access setting" enabled: - 1. On your bucket's dashboard, click on the Permissions tab. - 2. Click on the Edit button of the Block public access (bucket settings) section. - 3. In the form that opens, don't toggle any checkboxes and click the "Save changes" button. - 4. Confirm saving the changes by entering `confirm` in the pop-up that shows. - 5. Back on the Permissions page, scroll to the Object Ownership section and click the Edit button. - 6. In the form that opens: - - Choose the "ACLs enabled" card. - - Click on the "Save changes" button. - 7. Back on the Permissions page, scroll to the "Access Control List (ACL)" section and click on the Edit button. - 8. In the form that opens, enable the Read permission for "Everyone (public access)". - 9. Check the "I understand the effects of these changes on my objects and buckets." checkbox. - 10. Click on the "Save changes" button. - -### MinIO - -- Create [DigitalOcean account](https://cloud.digitalocean.com/registrations/new). -- Create [DigitalOcean Spaces bucket](https://docs.digitalocean.com/products/spaces/how-to/create/). -- Create [DigitalOcean Spaces access and secret access keys](https://docs.digitalocean.com/products/spaces/how-to/manage-access/#access-keys). - -### DigitalOcean Spaces - -1. Create a [Cloudflare account](https://dash.cloudflare.com/sign-up). -2. Set up your R2 bucket: - - Navigate to R2 Object Storage in your dashboard. You may need to provide your credit-card information. - - Click "Create bucket" - - Enter a unique bucket name - - Select "Automatic" for location - - Choose "Standard" for storage class - - Confirm by clicking "Create bucket" -3. Configure public access: - - On your bucket's dashboard, click on the Settings tab. - - Scroll down to the Public Access section, and click on "Allow Access" in the "R2.dev subdomain" card. - - Type 'allow' to confirm - - Copy the Public R2.dev Bucket URL for your `S3_FILE_URL` -4. Retrieve credentials: - - [Go to API tokens page](https://dash.cloudflare.com/?to=/:account/r2/api-tokens): - - Select "Create API token" - - Edit the "R2 Token" name - - Under Permissions, select Object Read & Write permission types - - You can optionally specify the buckets that this API token has access to under the "Specify bucket(s)" section. - - Once done, click the "Create API Token" button. - - You'll receive an access key ID and a secret access key. Save them to use them later for the `S3_ACCESS_KEY_ID` and `S3_SECRET_ACCESS_KEY` environment variables. - -### Supabase S3 Storage - -### Cloudflare R2 - -*** - -## Register the S3 File Module - -Add the module into the `providers` array of the File Module: - -The File Module accepts one provider only. - -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" - -// ... - -module.exports = { - // ... - modules: [ - // ... - { - resolve: "@medusajs/medusa/file", - options: { - providers: [ - { - resolve: "@medusajs/medusa/file-s3", - id: "s3", - options: { - file_url: process.env.S3_FILE_URL, - access_key_id: process.env.S3_ACCESS_KEY_ID, - secret_access_key: process.env.S3_SECRET_ACCESS_KEY, - region: process.env.S3_REGION, - bucket: process.env.S3_BUCKET, - endpoint: process.env.S3_ENDPOINT, - // other options... - }, - }, - ], - }, - }, - ], -} -``` - -### Additional Configuration for MinIO and Supabase - -If you're using MinIO or Supabase, set `forcePathStyle` to `true` in the `additional_client_config` object. - -For example: - -```ts title="medusa-config.ts" -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/file", - options: { - providers: [ - { - resolve: "@medusajs/medusa/file-s3", - id: "s3", - options: { - // ... - additional_client_config: { - forcePathStyle: true, - }, - }, - }, - ], - }, - }, - ], -}) -``` - -### S3 File Module Options - -|Option|Description|Default| -|---|---|---|---|---| -|\`file\_url\`|The base URL to upload files to.|-| -|\`access\_key\_id\`|The AWS or (S3 compatible) user's access key ID.|-| -|\`secret\_access\_key\`|The AWS or (S3 compatible) user's secret access key.|-| -|\`region\`|The bucket's region code.|-| -|\`bucket\`|The bucket's name.|-| -|\`endpoint\`|The URL to the AWS S3 (or compatible S3 API) server.|-| -|\`prefix\`|A string to prefix each uploaded file's name.|-| -|\`cache\_control\`|A string indicating how long objects remain in the AWS S3 (or compatible S3 API) cache.|\`public, max-age=31536000\`| -|\`download\_file\_duration\`|A number indicating the expiry time of presigned URLs in seconds.|\`3600\`| -|\`additional\_client\_config\`|Any additional configurations to pass to the S3 client.|-| - -*** - -## Troubleshooting - - -# Local Event Module - -The Local Event Module uses Node EventEmitter to implement Medusa's pub/sub events system. The Node EventEmitter is limited to a single process environment. - -This module is useful for development and testing, but it’s not recommended to be used in production. - -For production, it’s recommended to use modules like [Redis Event Bus Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/redis/index.html.md). - -*** - -## Register the Local Event Module - -The Local Event 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/event-bus-local", - }, - ], -}) -``` - -*** - -## 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 -Local Event Bus installed. This is not recommended for production. -``` - - -# How to Create an Event Module - -In this guide, you’ll learn how to create an Event Module. - -## 1. Create Module Directory - -Start by creating a new directory for your module. For example, `src/modules/my-event`. - -*** - -## 2. Create the Event Service - -Create the file `src/modules/my-event/service.ts` that holds the implementation of the event service. - -The Event Module's main service must extend the `AbstractEventBusModuleService` class from the Medusa Framework: - -```ts title="src/modules/my-event/service.ts" -import { AbstractEventBusModuleService } from "@medusajs/framework/utils" -import { Message } from "@medusajs/types" - -class MyEventService extends AbstractEventBusModuleService { - async emit(data: Message | Message[], options: Record): Promise { - throw new Error("Method not implemented.") - } - async releaseGroupedEvents(eventGroupId: string): Promise { - throw new Error("Method not implemented.") - } - async clearGroupedEvents(eventGroupId: string): Promise { - throw new Error("Method not implemented.") - } -} - -export default MyEventService -``` - -The service implements the required methods based on the desired publish/subscribe logic. - -### eventToSubscribersMap\_ Property - -The `AbstractEventBusModuleService` has a field `eventToSubscribersMap_`, which is a JavaScript Map. The map's keys are the event names, whereas the value of each key is an array of subscribed handler functions. - -In your custom implementation, you can use this property to manage the subscribed handler functions: - -```ts -const eventSubscribers = - this.eventToSubscribersMap_.get(eventName) || [] -``` - -### emit Method - -The `emit` method is used to push an event from the Medusa application into your messaging system. The subscribers to that event would then pick up the message and execute their asynchronous tasks. - -An example implementation: - -```ts title="src/modules/my-event/service.ts" -class MyEventService extends AbstractEventBusModuleService { - async emit(data: Message | Message[], options: Record): Promise { - const events = Array.isArray(data) ? data : [data] - - for (const event of events) { - console.log(`Received the event ${event.name} with data ${event.data}`) - - // TODO push the event somewhere - } - } - // ... -} -``` - -The `emit` method receives the following parameters: - -- data: (\`object or array of objects\`) The emitted event(s). - - - name: (\`string\`) The name of the emitted event. - - - data: (\`object\`) The data payload of the event. - - - metadata: (\`object\`) Additional details of the emitted event. - - - eventGroupId: (string) A group ID that the event belongs to. - - - options: (\`object\`) Additional options relevant for the event service. - -### releaseGroupedEvents Method - -Grouped events are useful when you have distributed transactions where you need to explicitly group, release, and clear events upon lifecycle transaction events. - -If your Event Module supports grouped events, this method is used to emit all events in a group, then clear that group. - -For example: - -```ts title="src/modules/my-event/service.ts" -class MyEventService extends AbstractEventBusModuleService { - protected groupedEventsMap_: Map - - constructor() { - // @ts-ignore - super(...arguments) - - this.groupedEventsMap_ = new Map() - } - - async releaseGroupedEvents(eventGroupId: string): Promise { - const groupedEvents = this.groupedEventsMap_.get(eventGroupId) || [] - - for (const event of groupedEvents) { - const { options, ...eventBody } = event - - // TODO emit event - } - - await this.clearGroupedEvents(eventGroupId) - } - - // ... -} -``` - -The `releaseGroupedEvents` receives the group ID as a parameter. - -In the example above, you add a `groupedEventsMap_` property to store grouped events. Then, in the method, you emit the events in the group, then clear the grouped events using the `clearGroupedEvents` which you'll learn about next. - -To add events to the grouped events map, you can do it in the `emit` method: - -```ts title="src/modules/my-event/service.ts" -class MyEventService extends AbstractEventBusModuleService { - // ... - async emit(data: Message | Message[], options: Record): Promise { - const events = Array.isArray(data) ? data : [data] - - for (const event of events) { - console.log(`Received the event ${event.name} with data ${event.data}`) - - if (event.metadata.eventGroupId) { - const groupedEvents = this.groupedEventsMap_.get( - event.metadata.eventGroupId - ) || [] - - groupedEvents.push(event) - - this.groupedEventsMap_.set(event.metadata.eventGroupId, groupedEvents) - continue - } - - // TODO push the event somewhere - } - } -} -``` - -### clearGroupedEvents Method - -If your Event Module supports grouped events, this method is used to remove the events of a group. - -For example: - -```ts title="src/modules/my-event/service.ts" -class MyEventService extends AbstractEventBusModuleService { - // from previous section - protected groupedEventsMap_: Map - - async clearGroupedEvents(eventGroupId: string): Promise { - this.groupedEventsMap_.delete(eventGroupId) - } - - // ... -} -``` - -The method accepts the group's name as a parameter. - -In the method, you delete the group from the `groupedEventsMap_` property (added in the previous section), deleting the stored events of it as well. - -*** - -## 3. Create Module Definition File - -Create the file `src/modules/my-event/index.ts` with the following content: - -```ts title="src/modules/my-event/index.ts" -import MyEventService from "./service" -import { Module } from "@medusajs/framework/utils" - -export default Module("my-event", { - service: MyEventService, -}) -``` - -This exports the module's definition, indicating that the `MyEventService` is the main service of the module. - -*** - -## 4. Use Module - -To use your Event Module, add it to the `modules` object exported as part of the configurations in `medusa-config.ts`. An Event Module is added under the `eventBus` key. - -For example: - -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" - -// ... - -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "./src/modules/my-event", - options: { - // any options - }, - }, - ], -}) -``` - - -# Redis Event Module - -The Redis Event Module uses Redis to implement Medusa's pub/sub events system. - -It's powered by BullMQ and `io-redis`. BullMQ is responsible for the message queue and worker, and `io-redis` is the underlying Redis client that BullMQ connects to for events storage. - -In production, it's recommended to use this module. - -*** - -## Register the Redis Event 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" -import { Modules } from "@medusajs/framework/utils" - -// ... - -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/event-bus-redis", - options: { - redisUrl: process.env.EVENTS_REDIS_URL, - }, - }, - ], -}) -``` - -### Environment Variables - -Make sure to add the following environment variables: - -```bash -EVENTS_REDIS_URL= -``` - -### Redis Event 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|-| -|\`queueName\`|A string indicating BullMQ's queue name.|No|\`events-queue\`| -|\`queueOptions\`|An object of options to pass to the BullMQ constructor. Refer to |No|-| -|\`workerOptions\`|An object of options to pass to the BullMQ Worker constructor. Refer to |No|-| -|\`jobOptions\`|An object of options to pass to jobs added to the BullMQ queue. Refer to |No|-| - -## 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 'event-redis' established -``` - - ## Workflows -- [deleteApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteApiKeysWorkflow/index.html.md) - [createApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/createApiKeysWorkflow/index.html.md) -- [updateApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateApiKeysWorkflow/index.html.md) -- [linkSalesChannelsToApiKeyWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkSalesChannelsToApiKeyWorkflow/index.html.md) - [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) +- [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) -- [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md) - [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) - [confirmVariantInventoryWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmVariantInventoryWorkflow/index.html.md) -- [createCartCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartCreditLinesWorkflow/index.html.md) -- [createPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentCollectionForCartWorkflow/index.html.md) -- [deleteCartCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCartCreditLinesWorkflow/index.html.md) -- [listShippingOptionsForCartWithPricingWorkflow](https://docs.medusajs.com/references/medusa-workflows/listShippingOptionsForCartWithPricingWorkflow/index.html.md) -- [listShippingOptionsForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/listShippingOptionsForCartWorkflow/index.html.md) - [createCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartWorkflow/index.html.md) -- [refreshCartShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartShippingMethodsWorkflow/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) -- [refreshPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshPaymentCollectionForCartWorkflow/index.html.md) +- [refreshCartShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartShippingMethodsWorkflow/index.html.md) +- [refundPaymentAndRecreatePaymentSessionWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentAndRecreatePaymentSessionWorkflow/index.html.md) - [transferCartCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/transferCartCustomerWorkflow/index.html.md) - [updateCartPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartPromotionsWorkflow/index.html.md) - [updateLineItemInCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateLineItemInCartWorkflow/index.html.md) -- [updateTaxLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxLinesWorkflow/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) -- [refundPaymentAndRecreatePaymentSessionWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentAndRecreatePaymentSessionWorkflow/index.html.md) -- [createCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAccountWorkflow/index.html.md) -- [deleteCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomerAddressesWorkflow/index.html.md) -- [createCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomersWorkflow/index.html.md) -- [deleteCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomersWorkflow/index.html.md) -- [createCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAddressesWorkflow/index.html.md) -- [updateCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerAddressesWorkflow/index.html.md) -- [removeCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeCustomerAccountWorkflow/index.html.md) -- [updateCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomersWorkflow/index.html.md) +- [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) -- [createLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/createLinksWorkflow/index.html.md) - [dismissLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/dismissLinksWorkflow/index.html.md) - [updateLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateLinksWorkflow/index.html.md) -- [deleteCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomerGroupsWorkflow/index.html.md) -- [createCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerGroupsWorkflow/index.html.md) -- [linkCustomerGroupsToCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkCustomerGroupsToCustomerWorkflow/index.html.md) -- [linkCustomersToCustomerGroupWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkCustomersToCustomerGroupWorkflow/index.html.md) -- [generateResetPasswordTokenWorkflow](https://docs.medusajs.com/references/medusa-workflows/generateResetPasswordTokenWorkflow/index.html.md) -- [updateCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerGroupsWorkflow/index.html.md) -- [createDefaultsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createDefaultsWorkflow/index.html.md) -- [calculateShippingOptionsPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/calculateShippingOptionsPricesWorkflow/index.html.md) -- [cancelFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelFulfillmentWorkflow/index.html.md) -- [batchShippingOptionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchShippingOptionRulesWorkflow/index.html.md) -- [createReturnFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnFulfillmentWorkflow/index.html.md) -- [createFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createFulfillmentWorkflow/index.html.md) -- [createServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createServiceZonesWorkflow/index.html.md) -- [createShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShippingOptionsWorkflow/index.html.md) -- [createShippingProfilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShippingProfilesWorkflow/index.html.md) -- [createShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShipmentWorkflow/index.html.md) -- [deleteFulfillmentSetsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteFulfillmentSetsWorkflow/index.html.md) -- [deleteShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteShippingOptionsWorkflow/index.html.md) -- [deleteServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteServiceZonesWorkflow/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) -- [markFulfillmentAsDeliveredWorkflow](https://docs.medusajs.com/references/medusa-workflows/markFulfillmentAsDeliveredWorkflow/index.html.md) -- [updateServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateServiceZonesWorkflow/index.html.md) -- [updateShippingProfilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateShippingProfilesWorkflow/index.html.md) -- [validateFulfillmentDeliverabilityStep](https://docs.medusajs.com/references/medusa-workflows/validateFulfillmentDeliverabilityStep/index.html.md) -- [uploadFilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/uploadFilesWorkflow/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) -- [addDraftOrderPromotionWorkflow](https://docs.medusajs.com/references/medusa-workflows/addDraftOrderPromotionWorkflow/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) +- [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) - [cancelDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelDraftOrderEditWorkflow/index.html.md) -- [convertDraftOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/convertDraftOrderWorkflow/index.html.md) -- [convertDraftOrderStep](https://docs.medusajs.com/references/medusa-workflows/convertDraftOrderStep/index.html.md) - [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) - [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) -- [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) - [updateDraftOrderActionShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderActionShippingMethodWorkflow/index.html.md) -- [updateDraftOrderShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderShippingMethodWorkflow/index.html.md) +- [removeDraftOrderShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderShippingMethodWorkflow/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) -- [updateDraftOrderActionItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderActionItemWorkflow/index.html.md) +- [updateDraftOrderShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderShippingMethodWorkflow/index.html.md) - [updateDraftOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderWorkflow/index.html.md) +- [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) +- [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) +- [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) +- [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) -- [createInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInventoryLevelsWorkflow/index.html.md) -- [deleteInventoryItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInventoryItemWorkflow/index.html.md) -- [deleteInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInventoryLevelsWorkflow/index.html.md) - [createInventoryItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInventoryItemsWorkflow/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) -- [updateInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateInventoryLevelsWorkflow/index.html.md) - [validateInventoryLevelsDelete](https://docs.medusajs.com/references/medusa-workflows/validateInventoryLevelsDelete/index.html.md) -- [deleteLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteLineItemsWorkflow/index.html.md) -- [acceptInviteWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptInviteWorkflow/index.html.md) -- [refreshInviteTokensWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshInviteTokensWorkflow/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) -- [capturePaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/capturePaymentWorkflow/index.html.md) +- [deleteLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteLineItemsWorkflow/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) -- [refundPaymentsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentsWorkflow/index.html.md) -- [validateRefundStep](https://docs.medusajs.com/references/medusa-workflows/validateRefundStep/index.html.md) - [validatePaymentsRefundStep](https://docs.medusajs.com/references/medusa-workflows/validatePaymentsRefundStep/index.html.md) -- [batchPriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchPriceListPricesWorkflow/index.html.md) +- [validateRefundStep](https://docs.medusajs.com/references/medusa-workflows/validateRefundStep/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) -- [deletePriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePriceListsWorkflow/index.html.md) - [createPriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListsWorkflow/index.html.md) -- [removePriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/removePriceListPricesWorkflow/index.html.md) +- [batchPriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchPriceListPricesWorkflow/index.html.md) +- [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) -- [createRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRefundReasonsWorkflow/index.html.md) -- [createPaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentSessionsWorkflow/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) -- [updateRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRefundReasonsWorkflow/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) -- [acceptOrderTransferValidationStep](https://docs.medusajs.com/references/medusa-workflows/acceptOrderTransferValidationStep/index.html.md) - [updatePricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePricePreferencesWorkflow/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) -- [beginClaimOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginClaimOrderWorkflow/index.html.md) -- [beginClaimOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginClaimOrderValidationStep/index.html.md) -- [beginExchangeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginExchangeOrderWorkflow/index.html.md) -- [beginOrderEditOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditOrderWorkflow/index.html.md) -- [beginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginOrderExchangeValidationStep/index.html.md) -- [beginOrderEditValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditValidationStep/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) -- [cancelBeginOrderClaimWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderClaimWorkflow/index.html.md) -- [cancelBeginOrderClaimValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderClaimValidationStep/index.html.md) -- [beginReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginReturnOrderWorkflow/index.html.md) -- [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) -- [cancelBeginOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditWorkflow/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) -- [cancelOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderExchangeWorkflow/index.html.md) -- [cancelExchangeValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelExchangeValidateOrder/index.html.md) -- [cancelOrderFulfillmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentValidateOrder/index.html.md) -- [cancelOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentWorkflow/index.html.md) -- [cancelReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelReceiveReturnValidationStep/index.html.md) -- [cancelOrderTransferRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderTransferRequestWorkflow/index.html.md) -- [cancelRequestReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelRequestReturnValidationStep/index.html.md) -- [cancelOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderWorkflow/index.html.md) -- [cancelReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnReceiveWorkflow/index.html.md) -- [cancelReturnValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelReturnValidateOrder/index.html.md) -- [cancelReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnWorkflow/index.html.md) -- [cancelReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnRequestWorkflow/index.html.md) -- [cancelValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelValidateOrder/index.html.md) -- [completeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeOrderWorkflow/index.html.md) -- [cancelTransferOrderRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelTransferOrderRequestValidationStep/index.html.md) -- [confirmClaimRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmClaimRequestValidationStep/index.html.md) -- [confirmExchangeRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestWorkflow/index.html.md) -- [confirmExchangeRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestValidationStep/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) -- [confirmReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmReceiveReturnValidationStep/index.html.md) -- [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/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) -- [createAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createAndCompleteReturnOrderWorkflow/index.html.md) -- [createClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createClaimShippingMethodValidationStep/index.html.md) -- [createClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createClaimShippingMethodWorkflow/index.html.md) -- [createExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createExchangeShippingMethodValidationStep/index.html.md) -- [createCompleteReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/createCompleteReturnValidationStep/index.html.md) -- [createExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createExchangeShippingMethodWorkflow/index.html.md) -- [createFulfillmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/createFulfillmentValidateOrder/index.html.md) -- [createOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderChangeActionsWorkflow/index.html.md) -- [createOrUpdateOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrUpdateOrderPaymentCollectionWorkflow/index.html.md) -- [createOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderChangeWorkflow/index.html.md) -- [createOrderCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderCreditLinesWorkflow/index.html.md) -- [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md) -- [createOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createOrderEditShippingMethodValidationStep/index.html.md) -- [createOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderEditShippingMethodWorkflow/index.html.md) -- [createOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderPaymentCollectionWorkflow/index.html.md) -- [createOrderShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderShipmentWorkflow/index.html.md) -- [createOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderWorkflow/index.html.md) -- [createOrdersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrdersWorkflow/index.html.md) -- [createReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createReturnShippingMethodValidationStep/index.html.md) -- [createReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnShippingMethodWorkflow/index.html.md) -- [createShipmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/createShipmentValidateOrder/index.html.md) -- [declineOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/declineOrderChangeWorkflow/index.html.md) -- [declineOrderTransferRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/declineOrderTransferRequestWorkflow/index.html.md) -- [deleteOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteOrderChangeActionsWorkflow/index.html.md) -- [deleteOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteOrderChangeWorkflow/index.html.md) -- [declineTransferOrderRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/declineTransferOrderRequestValidationStep/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) -- [fetchShippingOptionForOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/fetchShippingOptionForOrderWorkflow/index.html.md) -- [getOrderDetailWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrderDetailWorkflow/index.html.md) -- [getOrdersListWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrdersListWorkflow/index.html.md) -- [maybeRefreshShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/maybeRefreshShippingMethodsWorkflow/index.html.md) -- [orderClaimAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimAddNewItemValidationStep/index.html.md) -- [orderClaimAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimAddNewItemWorkflow/index.html.md) -- [markPaymentCollectionAsPaid](https://docs.medusajs.com/references/medusa-workflows/markPaymentCollectionAsPaid/index.html.md) -- [markOrderFulfillmentAsDeliveredWorkflow](https://docs.medusajs.com/references/medusa-workflows/markOrderFulfillmentAsDeliveredWorkflow/index.html.md) -- [orderClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimItemValidationStep/index.html.md) -- [orderClaimItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimItemWorkflow/index.html.md) -- [orderClaimRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimRequestItemReturnValidationStep/index.html.md) -- [orderClaimRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimRequestItemReturnWorkflow/index.html.md) -- [orderEditAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderEditAddNewItemWorkflow/index.html.md) -- [orderEditAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderEditAddNewItemValidationStep/index.html.md) -- [orderEditUpdateItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderEditUpdateItemQuantityValidationStep/index.html.md) -- [orderFulfillmentDeliverablilityValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderFulfillmentDeliverablilityValidationStep/index.html.md) -- [orderExchangeRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeRequestItemReturnWorkflow/index.html.md) -- [orderExchangeAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeAddNewItemWorkflow/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) -- [orderEditUpdateItemQuantityWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderEditUpdateItemQuantityWorkflow/index.html.md) -- [receiveItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestValidationStep/index.html.md) -- [receiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestWorkflow/index.html.md) -- [removeClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimShippingMethodValidationStep/index.html.md) -- [removeClaimItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimItemActionValidationStep/index.html.md) -- [removeAddItemClaimActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeAddItemClaimActionWorkflow/index.html.md) -- [removeClaimAddItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimAddItemActionValidationStep/index.html.md) -- [removeClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeClaimShippingMethodWorkflow/index.html.md) -- [removeExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeShippingMethodValidationStep/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) -- [removeItemOrderEditActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemOrderEditActionWorkflow/index.html.md) -- [removeItemExchangeActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemExchangeActionWorkflow/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) -- [removeItemReturnActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemReturnActionWorkflow/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) -- [removeOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditShippingMethodWorkflow/index.html.md) -- [removeReturnItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeReturnItemActionValidationStep/index.html.md) -- [removeReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeReturnShippingMethodValidationStep/index.html.md) -- [removeOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditShippingMethodValidationStep/index.html.md) -- [requestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestItemReturnWorkflow/index.html.md) -- [requestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestItemReturnValidationStep/index.html.md) -- [removeReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeReturnShippingMethodWorkflow/index.html.md) -- [requestOrderEditRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestOrderEditRequestValidationStep/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) -- [requestOrderTransferValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestOrderTransferValidationStep/index.html.md) -- [throwUnlessStatusIsNotPaid](https://docs.medusajs.com/references/medusa-workflows/throwUnlessStatusIsNotPaid/index.html.md) -- [updateClaimAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimAddItemWorkflow/index.html.md) -- [updateClaimAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimAddItemValidationStep/index.html.md) -- [throwUnlessPaymentCollectionNotPaid](https://docs.medusajs.com/references/medusa-workflows/throwUnlessPaymentCollectionNotPaid/index.html.md) -- [updateClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemValidationStep/index.html.md) -- [updateClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimShippingMethodWorkflow/index.html.md) -- [updateClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimShippingMethodValidationStep/index.html.md) -- [updateClaimItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemWorkflow/index.html.md) -- [updateExchangeAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateExchangeAddItemValidationStep/index.html.md) -- [updateExchangeAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateExchangeAddItemWorkflow/index.html.md) -- [updateExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateExchangeShippingMethodWorkflow/index.html.md) -- [updateExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateExchangeShippingMethodValidationStep/index.html.md) -- [updateOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderChangeActionsWorkflow/index.html.md) -- [updateOrderChangesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderChangesWorkflow/index.html.md) -- [updateOrderEditAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditAddItemValidationStep/index.html.md) -- [updateOrderEditAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditAddItemWorkflow/index.html.md) -- [updateOrderEditItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityValidationStep/index.html.md) -- [updateOrderTaxLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderTaxLinesWorkflow/index.html.md) -- [updateOrderEditItemQuantityWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityWorkflow/index.html.md) -- [updateOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditShippingMethodWorkflow/index.html.md) -- [updateOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditShippingMethodValidationStep/index.html.md) -- [updateOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderValidationStep/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) -- [updateRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnValidationStep/index.html.md) -- [updateRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnWorkflow/index.html.md) -- [updateReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnShippingMethodWorkflow/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) -- [updateReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnWorkflow/index.html.md) -- [validateOrderCreditLinesStep](https://docs.medusajs.com/references/medusa-workflows/validateOrderCreditLinesStep/index.html.md) -- [deleteProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductCategoriesWorkflow/index.html.md) -- [createProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductCategoriesWorkflow/index.html.md) -- [updateProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductCategoriesWorkflow/index.html.md) -- [batchPromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchPromotionRulesWorkflow/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) -- [createPromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionRulesWorkflow/index.html.md) -- [deleteCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCampaignsWorkflow/index.html.md) -- [createPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionsWorkflow/index.html.md) -- [deletePromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePromotionRulesWorkflow/index.html.md) -- [deletePromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePromotionsWorkflow/index.html.md) -- [updateCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCampaignsWorkflow/index.html.md) -- [updatePromotionsStatusWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsStatusWorkflow/index.html.md) -- [updatePromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionRulesWorkflow/index.html.md) -- [updatePromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsWorkflow/index.html.md) -- [updatePromotionsValidationStep](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsValidationStep/index.html.md) -- [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) -- [createReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnReasonsWorkflow/index.html.md) -- [updateReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnReasonsWorkflow/index.html.md) -- [deleteReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReturnReasonsWorkflow/index.html.md) -- [deleteReservationsByLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReservationsByLineItemsWorkflow/index.html.md) -- [createReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReservationsWorkflow/index.html.md) -- [updateReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReservationsWorkflow/index.html.md) -- [deleteReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReservationsWorkflow/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) +- [deletePricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePricePreferencesWorkflow/index.html.md) +- [createPricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPricePreferencesWorkflow/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) - [createCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCollectionsWorkflow/index.html.md) - [createProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductOptionsWorkflow/index.html.md) - [createProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTagsWorkflow/index.html.md) - [createProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTypesWorkflow/index.html.md) - [deleteCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCollectionsWorkflow/index.html.md) -- [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/index.html.md) - [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/index.html.md) -- [deleteProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductVariantsWorkflow/index.html.md) -- [deleteProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTypesWorkflow/index.html.md) +- [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/index.html.md) - [deleteProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductOptionsWorkflow/index.html.md) +- [deleteProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTypesWorkflow/index.html.md) - [deleteProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTagsWorkflow/index.html.md) -- [exportProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/exportProductsWorkflow/index.html.md) +- [deleteProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductVariantsWorkflow/index.html.md) - [deleteProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductsWorkflow/index.html.md) +- [exportProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/exportProductsWorkflow/index.html.md) +- [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) -- [updateProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductOptionsWorkflow/index.html.md) - [updateProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTypesWorkflow/index.html.md) -- [updateProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductVariantsWorkflow/index.html.md) -- [upsertVariantPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/upsertVariantPricesWorkflow/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) +- [updateProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductVariantsWorkflow/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) +- [createProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductCategoriesWorkflow/index.html.md) +- [deleteProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductCategoriesWorkflow/index.html.md) +- [updateProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductCategoriesWorkflow/index.html.md) +- [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) +- [batchPromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchPromotionRulesWorkflow/index.html.md) +- [createCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCampaignsWorkflow/index.html.md) +- [createPromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionRulesWorkflow/index.html.md) +- [createPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionsWorkflow/index.html.md) +- [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) +- [updateCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCampaignsWorkflow/index.html.md) +- [updatePromotionsStatusWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsStatusWorkflow/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) +- [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) @@ -34120,291 +34167,295 @@ Connection to Redis in module 'event-redis' established - [validateStepShippingProfileDelete](https://docs.medusajs.com/references/medusa-workflows/validateStepShippingProfileDelete/index.html.md) - [deleteShippingProfileWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteShippingProfileWorkflow/index.html.md) - [createStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createStockLocationsWorkflow/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) -- [updateStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateStockLocationsWorkflow/index.html.md) +- [createLocationFulfillmentSetWorkflow](https://docs.medusajs.com/references/medusa-workflows/createLocationFulfillmentSetWorkflow/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) -- [createTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRegionsWorkflow/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) - [deleteTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRateRulesWorkflow/index.html.md) -- [maybeListTaxRateRuleIdsStep](https://docs.medusajs.com/references/medusa-workflows/maybeListTaxRateRuleIdsStep/index.html.md) - [deleteTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRegionsWorkflow/index.html.md) - [deleteTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRatesWorkflow/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) -- [createStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/createStoresWorkflow/index.html.md) - [updateTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxRegionsWorkflow/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) -- [createUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUsersWorkflow/index.html.md) - [createUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUserAccountWorkflow/index.html.md) - [deleteUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteUsersWorkflow/index.html.md) -- [updateUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateUsersWorkflow/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) -- [revokeApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/revokeApiKeysStep/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) -- [createCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCartsStep/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) -- [createPaymentCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentCollectionsStep/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) -- [getVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantsStep/index.html.md) - [getPromotionCodesToApply](https://docs.medusajs.com/references/medusa-workflows/steps/getPromotionCodesToApply/index.html.md) -- [prepareAdjustmentsFromPromotionActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/prepareAdjustmentsFromPromotionActionsStep/index.html.md) - [getVariantPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantPriceSetsStep/index.html.md) -- [removeShippingMethodAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeShippingMethodAdjustmentsStep/index.html.md) +- [prepareAdjustmentsFromPromotionActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/prepareAdjustmentsFromPromotionActionsStep/index.html.md) +- [getVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantsStep/index.html.md) - [removeLineItemAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeLineItemAdjustmentsStep/index.html.md) - [removeShippingMethodFromCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeShippingMethodFromCartStep/index.html.md) -- [setTaxLinesForItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/setTaxLinesForItemsStep/index.html.md) -- [retrieveCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/retrieveCartStep/index.html.md) +- [removeShippingMethodAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeShippingMethodAdjustmentsStep/index.html.md) - [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) -- [updateCartPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartPromotionsStep/index.html.md) -- [validateCartPaymentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartPaymentsStep/index.html.md) - [validateAndReturnShippingMethodsDataStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateAndReturnShippingMethodsDataStep/index.html.md) +- [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) -- [validateShippingStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShippingStep/index.html.md) -- [validateLineItemPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateLineItemPricesStep/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) -- [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) -- [maybeUnsetDefaultBillingAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/maybeUnsetDefaultBillingAddressesStep/index.html.md) -- [deleteCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomersStep/index.html.md) -- [createCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomersStep/index.html.md) -- [maybeUnsetDefaultShippingAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/maybeUnsetDefaultShippingAddressesStep/index.html.md) -- [validateCustomerAccountCreation](https://docs.medusajs.com/references/medusa-workflows/steps/validateCustomerAccountCreation/index.html.md) -- [updateCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomersStep/index.html.md) -- [updateCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomerAddressesStep/index.html.md) +- [createEntitiesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createEntitiesStep/index.html.md) - [createRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRemoteLinkStep/index.html.md) -- [dismissRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/dismissRemoteLinkStep/index.html.md) - [emitEventStep](https://docs.medusajs.com/references/medusa-workflows/steps/emitEventStep/index.html.md) -- [removeRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeRemoteLinkStep/index.html.md) +- [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) +- [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) - [useRemoteQueryStep](https://docs.medusajs.com/references/medusa-workflows/steps/useRemoteQueryStep/index.html.md) -- [setAuthAppMetadataStep](https://docs.medusajs.com/references/medusa-workflows/steps/setAuthAppMetadataStep/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) - [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) -- [linkCustomersToCustomerGroupStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkCustomersToCustomerGroupStep/index.html.md) - [linkCustomerGroupsToCustomerStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkCustomerGroupsToCustomerStep/index.html.md) +- [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) - [uploadFilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/uploadFilesStep/index.html.md) - [adjustInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/adjustInventoryLevelsStep/index.html.md) - [attachInventoryItemToVariants](https://docs.medusajs.com/references/medusa-workflows/steps/attachInventoryItemToVariants/index.html.md) - [createInventoryItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInventoryItemsStep/index.html.md) -- [deleteInventoryItemStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInventoryItemStep/index.html.md) -- [deleteInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInventoryLevelsStep/index.html.md) -- [updateInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryLevelsStep/index.html.md) -- [updateInventoryItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryItemsStep/index.html.md) - [createInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInventoryLevelsStep/index.html.md) -- [validateInventoryDeleteStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryDeleteStep/index.html.md) -- [validateInventoryLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryLocationsStep/index.html.md) +- [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) -- [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) +- [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) +- [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) -- [createFulfillmentSets](https://docs.medusajs.com/references/medusa-workflows/steps/createFulfillmentSets/index.html.md) -- [createFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/createFulfillmentStep/index.html.md) - [createReturnFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnFulfillmentStep/index.html.md) -- [createServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createServiceZonesStep/index.html.md) +- [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) -- [createShippingOptionsPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingOptionsPriceSetsStep/index.html.md) -- [deleteServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteServiceZonesStep/index.html.md) -- [deleteFulfillmentSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteFulfillmentSetsStep/index.html.md) +- [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) -- [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) +- [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) -- [updateShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingOptionRulesStep/index.html.md) -- [updateShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingProfilesStep/index.html.md) +- [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) -- [validateDraftOrderStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateDraftOrderStep/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) -- [archiveOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/archiveOrdersStep/index.html.md) +- [createInviteStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInviteStep/index.html.md) +- [deleteInvitesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInvitesStep/index.html.md) +- [refreshInviteTokensStep](https://docs.medusajs.com/references/medusa-workflows/steps/refreshInviteTokensStep/index.html.md) +- [validateTokenStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateTokenStep/index.html.md) +- [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) - [addOrderTransactionStep](https://docs.medusajs.com/references/medusa-workflows/steps/addOrderTransactionStep/index.html.md) -- [cancelOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderChangeStep/index.html.md) -- [cancelOrderExchangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderExchangeStep/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) -- [cancelOrderReturnStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderReturnStep/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) - [cancelOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrdersStep/index.html.md) -- [createCompleteReturnStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCompleteReturnStep/index.html.md) -- [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) +- [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) - [createOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderChangeStep/index.html.md) - [createOrderClaimsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderClaimsStep/index.html.md) -- [createOrderExchangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderExchangesStep/index.html.md) -- [createOrderLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderLineItemsStep/index.html.md) - [createOrderExchangeItemsFromActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderExchangeItemsFromActionsStep/index.html.md) +- [createOrderLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderLineItemsStep/index.html.md) +- [createReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnsStep/index.html.md) +- [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) -- [createReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnsStep/index.html.md) -- [declineOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/declineOrderChangeStep/index.html.md) -- [deleteOrderChangeActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderChangeActionsStep/index.html.md) -- [deleteOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderChangesStep/index.html.md) -- [deleteOrderLineItems](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderLineItems/index.html.md) - [deleteOrderShippingMethods](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderShippingMethods/index.html.md) -- [deleteReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReturnsStep/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) -- [registerOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderChangesStep/index.html.md) +- [deleteReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReturnsStep/index.html.md) - [registerOrderDeliveryStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderDeliveryStep/index.html.md) +- [registerOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderChangesStep/index.html.md) - [registerOrderFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderFulfillmentStep/index.html.md) - [registerOrderShipmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderShipmentStep/index.html.md) - [setOrderTaxLinesForItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/setOrderTaxLinesForItemsStep/index.html.md) - [updateOrderChangeActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderChangeActionsStep/index.html.md) - [updateOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderChangesStep/index.html.md) +- [updateReturnItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnItemsStep/index.html.md) - [updateOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrdersStep/index.html.md) - [updateOrderShippingMethodsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderShippingMethodsStep/index.html.md) - [updateReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnsStep/index.html.md) -- [updateReturnItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnItemsStep/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) - [authorizePaymentSessionStep](https://docs.medusajs.com/references/medusa-workflows/steps/authorizePaymentSessionStep/index.html.md) - [cancelPaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelPaymentStep/index.html.md) - [refundPaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/refundPaymentStep/index.html.md) -- [refundPaymentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/refundPaymentsStep/index.html.md) - [capturePaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/capturePaymentStep/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) -- [createPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceSetsStep/index.html.md) -- [updatePricePreferencesAsArrayStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePricePreferencesAsArrayStep/index.html.md) -- [updatePricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePricePreferencesStep/index.html.md) -- [createPaymentAccountHolderStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentAccountHolderStep/index.html.md) -- [updatePriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceSetsStep/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) -- [updatePaymentCollectionStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePaymentCollectionStep/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) +- [createPriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceListPricesStep/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) - [batchLinkProductsToCategoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/batchLinkProductsToCategoryStep/index.html.md) -- [batchLinkProductsToCollectionStep](https://docs.medusajs.com/references/medusa-workflows/steps/batchLinkProductsToCollectionStep/index.html.md) -- [createCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCollectionsStep/index.html.md) - [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) -- [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) +- [createProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductVariantsStep/index.html.md) - [createProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductTagsStep/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) -- [deleteProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductTagsStep/index.html.md) - [deleteProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductOptionsStep/index.html.md) -- [deleteProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductVariantsStep/index.html.md) - [deleteProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductTypesStep/index.html.md) +- [deleteProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductTagsStep/index.html.md) +- [deleteProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductVariantsStep/index.html.md) - [deleteProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductsStep/index.html.md) -- [getVariantAvailabilityStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantAvailabilityStep/index.html.md) - [generateProductCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/generateProductCsvStep/index.html.md) -- [getProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getProductsStep/index.html.md) - [getAllProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getAllProductsStep/index.html.md) +- [getProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getProductsStep/index.html.md) +- [getVariantAvailabilityStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantAvailabilityStep/index.html.md) - [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) +- [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) - [updateProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductTypesStep/index.html.md) -- [updateProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductTagsStep/index.html.md) - [updateProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductVariantsStep/index.html.md) -- [waitConfirmationProductImportStep](https://docs.medusajs.com/references/medusa-workflows/steps/waitConfirmationProductImportStep/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) -- [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) -- [getExistingPriceListsPriceIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getExistingPriceListsPriceIdsStep/index.html.md) -- [removePriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/removePriceListPricesStep/index.html.md) -- [deletePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePriceListsStep/index.html.md) -- [updatePriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceListPricesStep/index.html.md) -- [updatePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceListsStep/index.html.md) -- [validateVariantPriceLinksStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateVariantPriceLinksStep/index.html.md) -- [validatePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validatePriceListsStep/index.html.md) -- [createInviteStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInviteStep/index.html.md) -- [refreshInviteTokensStep](https://docs.medusajs.com/references/medusa-workflows/steps/refreshInviteTokensStep/index.html.md) -- [deleteInvitesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInvitesStep/index.html.md) -- [validateTokenStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateTokenStep/index.html.md) -- [createReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReservationsStep/index.html.md) -- [updateReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReservationsStep/index.html.md) -- [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) - [associateLocationsWithSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/associateLocationsWithSalesChannelsStep/index.html.md) - [associateProductsWithSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/associateProductsWithSalesChannelsStep/index.html.md) - [canDeleteSalesChannelsOrThrowStep](https://docs.medusajs.com/references/medusa-workflows/steps/canDeleteSalesChannelsOrThrowStep/index.html.md) -- [createDefaultSalesChannelStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultSalesChannelStep/index.html.md) - [createSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createSalesChannelsStep/index.html.md) +- [createDefaultSalesChannelStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultSalesChannelStep/index.html.md) - [deleteSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteSalesChannelsStep/index.html.md) -- [updateSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateSalesChannelsStep/index.html.md) -- [detachProductsFromSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/detachProductsFromSalesChannelsStep/index.html.md) - [detachLocationsFromSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/detachLocationsFromSalesChannelsStep/index.html.md) -- [listShippingOptionsForContextStep](https://docs.medusajs.com/references/medusa-workflows/steps/listShippingOptionsForContextStep/index.html.md) -- [deleteShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingProfilesStep/index.html.md) +- [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) -- [updateReturnReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnReasonsStep/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) - [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) -- [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) -- [createStockLocations](https://docs.medusajs.com/references/medusa-workflows/steps/createStockLocations/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) -- [addCampaignPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/addCampaignPromotionsStep/index.html.md) -- [createPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPromotionsStep/index.html.md) -- [deletePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePromotionsStep/index.html.md) -- [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) -- [removeCampaignPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeCampaignPromotionsStep/index.html.md) -- [updatePromotionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePromotionRulesStep/index.html.md) -- [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) - [createTaxRateRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRateRulesStep/index.html.md) -- [deleteTaxRateRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRateRulesStep/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) -- [listTaxRateIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listTaxRateIdsStep/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) -- [updateTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateTaxRatesStep/index.html.md) -- [updateTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateTaxRegionsStep/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) -- [updateUsersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateUsersStep/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) +- [updateUsersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateUsersStep/index.html.md) # Events Reference @@ -35910,6 +35961,67 @@ 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. @@ -36030,67 +36142,6 @@ npx medusa db:sync-links |\`--execute-all\`|Skip prompts when syncing links and execute all (including unsafe) actions.|No|Prompts are shown for unsafe actions, by default.| -# develop Command - Medusa CLI Reference - -Start Medusa application in development. This command watches files for any changes, then rebuilds the files and restarts the Medusa application. - -```bash -npx medusa develop -``` - -## Options - -|Option|Description|Default| -|---|---|---|---|---| -|\`-H \\`|Set host of the Medusa server.|\`localhost\`| -|\`-p \\`|Set port of the Medusa server.|\`9000\`| - - -# 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.| - - # 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. @@ -36169,22 +36220,6 @@ npx medusa start |\`--cluster \\`|Start Medusa's Node.js server in |Cluster mode is disabled by default. If the option is passed but no number is passed, Medusa will try to consume all available CPU cores.| -# 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.| - - # user Command - Medusa CLI Reference Create a new admin user. @@ -36204,6 +36239,22 @@ npx medusa user --email [--password ] If ran successfully, you'll receive the invite token in the output.|No|\`false\`| +# telemetry Command - Medusa CLI Reference + +Enable or disable the collection of anonymous data usage. If no option is provided, the command enables the collection of anonymous data usage. + +```bash +npx medusa telemetry +``` + +#### Options + +|Option|Description| +|---|---|---| +|\`--enable\`|Enable telemetry (default).| +|\`--disable\`|Disable telemetry.| + + # Medusa CLI Reference The Medusa CLI tool provides commands that facilitate your development. @@ -36227,22 +36278,6 @@ npx medusa --help *** -# 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. @@ -36305,6 +36340,22 @@ By default, the Medusa Admin is built to the `.medusa/server/public/admin` direc If you want a separate build to host the admin standalone, such as on Vercel, pass the `--admin-only` option as explained in the [Options](#options) section. This outputs the admin to the `.medusa/admin` directory instead. +# develop Command - Medusa CLI Reference + +Start Medusa application in development. This command watches files for any changes, then rebuilds the files and restarts the Medusa application. + +```bash +npx medusa develop +``` + +## Options + +|Option|Description|Default| +|---|---|---|---|---| +|\`-H \\`|Set host of the Medusa server.|\`localhost\`| +|\`-p \\`|Set port of the Medusa server.|\`9000\`| + + # 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). @@ -36321,148 +36372,6 @@ npx medusa exec [file] [args...] |\`args\`|A list of arguments to pass to the function. These arguments are passed in the |No| -# plugin Commands - Medusa CLI Reference - -Commands starting with `plugin:` perform actions related to [plugin](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md) development. - -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.| - - -# 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.| - - -# user Command - Medusa CLI Reference - -Create a new admin user. - -```bash -npx medusa user --email [--password ] -``` - -## Options - -|Option|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`-e \\`|The user's email.|Yes|-| -|\`-p \\`|The user's password.|No|-| -|\`-i \\`|The user's ID.|No|An automatically generated ID.| -|\`--invite\`|Whether to create an invite instead of a user. When using this option, you don't need to specify a password. -If ran successfully, you'll receive the invite token in the output.|No|\`false\`| - - -# 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.| - - # db Commands - Medusa CLI Reference Commands starting with `db:` perform actions on the database. @@ -36583,6 +36492,148 @@ npx medusa db:sync-links |\`--execute-all\`|Skip prompts when syncing links and execute all (including unsafe) actions.|No|Prompts are shown for unsafe actions, by default.| +# new Command - Medusa CLI Reference + +Create a new Medusa application. Unlike the `create-medusa-app` CLI tool, this command provides more flexibility for experienced Medusa developers in creating and configuring their project. + +```bash +medusa new [ []] +``` + +## Arguments + +|Argument|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`dir\_name\`|The name of the directory to create the Medusa application in.|Yes|-| +|\`starter\_url\`|The URL of the starter repository to create the project from.|No|\`https://github.com/medusajs/medusa-starter-default\`| + +## Options + +|Option|Description| +|---|---|---| +|\`-y\`|Skip all prompts, such as databaes prompts. A database might not be created if default PostgreSQL credentials don't work.| +|\`--skip-db\`|Skip database creation.| +|\`--skip-env\`|Skip populating | +|\`--db-user \\`|The database user to use for database setup.| +|\`--db-database \\`|The name of the database used for database setup.| +|\`--db-pass \\`|The database password to use for database setup.| +|\`--db-port \\`|The database port to use for database setup.| +|\`--db-host \\`|The database host to use for database setup.| + + +# 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.| + + +# 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 +``` + + +# 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.| + + +# user Command - Medusa CLI Reference + +Create a new admin user. + +```bash +npx medusa user --email [--password ] +``` + +## Options + +|Option|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`-e \\`|The user's email.|Yes|-| +|\`-p \\`|The user's password.|No|-| +|\`-i \\`|The user's ID.|No|An automatically generated ID.| +|\`--invite\`|Whether to create an invite instead of a user. When using this option, you don't need to specify a password. +If ran successfully, you'll receive the invite token in the output.|No|\`false\`| + + # Medusa JS SDK In this documentation, you'll learn how to install and use Medusa's JS SDK. @@ -36616,7 +36667,7 @@ In your project, create the following `config.ts` file: For admin customizations, create this file at `src/admin/lib/config.ts`. -### Admin +### Admin (Medusa project) ```ts title="src/admin/lib/config.ts" import Medusa from "@medusajs/js-sdk" @@ -36630,6 +36681,20 @@ export const sdk = new Medusa({ }) ``` +### Admin (Medusa Plugin) + +```ts title="src/admin/lib/config.ts" +import Medusa from "@medusajs/js-sdk" + +export const sdk = new Medusa({ + baseUrl: __BACKEND_URL__ || "/", + debug: import.meta.env.DEV, + auth: { + type: "session", + }, +}) +``` + ### Storefront ```ts title="config.ts" @@ -36648,7 +36713,7 @@ export const sdk = new Medusa({ }) ``` -In the Medusa Admin you use `import.meta.env` to access environment variables becaues the Medusa Admin is built on top of [Vite](https://vitejs.dev/). Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/environment-variables/index.html.md). +In Medusa Admin customizations that are created in a Medusa project, you use `import.meta.env` to access environment variables, whereas in customizations built in a Medusa 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/docs/learn/fundamentals/admin/environment-variables/index.html.md) chapter. ### JS SDK Configurations @@ -45722,28 +45787,32 @@ 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 +# Implement Loyalty Points System in Medusa -In this tutorial, you will learn how to send notifications to customers who have abandoned their carts. +In this tutorial, you'll learn how to implement a loyalty points system 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. These features include cart-management capabilities. +Medusa Cloud provides a beta Store Credits feature that facilitates building a loyalty point system. [Get in touch](https://medusajs.com/contact) for early access. -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. +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 management capabilities related to carts, orders, promotions, and more. -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. +A loyalty point system allows customers to earn points for purchases, which can be redeemed for discounts or rewards. In this tutorial, you'll learn how to customize the Medusa application to implement a loyalty points system. + +You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer. ## 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. +- Define models to store loyalty points and the logic to manage them. +- Build flows that allow customers to earn and redeem points during checkout. + - Points are redeemed through dynamic promotions specific to the customer. +- Customize the cart completion flow to validate applied loyalty points. -![Diagram illustrating the flow of the abandoned-cart functionalities](https://res.cloudinary.com/dza7lstvk/image/upload/v1742460588/Medusa%20Resources/abandoned-cart-summary_fcf2tn.jpg) +![Diagram illustrating redeem loyalty points flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1744126213/Medusa%20Resources/redeem-points-flow_kzgkux.jpg) -[View on Github](https://github.com/medusajs/examples/tree/main/abandoned-cart): Find the full code for this tutorial. +- [Loyalty Points Repository](https://github.com/medusajs/examples/tree/main/loyalty-points): Find the full code for this guide in this repository. +- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1744212595/OpenApi/Loyalty-Points_jwi5e9.yaml): Import this OpenApi Specs file into tools like Postman. *** @@ -45761,594 +45830,1750 @@ Start by installing the Medusa application on your machine with the following co 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." +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: Set up SendGrid +## Step 2: Create Loyalty Module -### Prerequisites +In Medusa, you can build custom features in a [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). 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. -- [SendGrid account](https://sendgrid.com) -- [Verified Sender Identity](https://mc.sendgrid.com/senders) -- [SendGrid API Key](https://app.sendgrid.com/settings/api_keys) +In the module, you define the data models necessary for a feature and the logic to manage these data models. Later, you can build commerce flows around your module. -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. +In this step, you'll build a Loyalty Module that defines the necessary data models to store and manage loyalty points for customers. -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. +Refer to the [Modules documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) to learn more. -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). +### Create Module Directory -To set up SendGrid, add the SendGrid Notification Module Provider to `medusa-config.ts`: +Modules are created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/loyalty`. + +### Create Data Models + +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. + +Refer to the [Data Models documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#1-create-data-model/index.html.md) to learn more. + +For the Loyalty Module, you need to define a `LoyaltyPoint` data model that represents a customer's loyalty points. So, create the file `src/modules/loyalty/models/loyalty-point.ts` with the following content: + +```ts title="src/modules/loyalty/models/loyalty-point.ts" highlights={dmlHighlights} +import { model } from "@medusajs/framework/utils" + +const LoyaltyPoint = model.define("loyalty_point", { + id: model.id().primaryKey(), + points: model.number().default(0), + customer_id: model.text().unique("IDX_LOYALTY_CUSTOMER_ID"), +}) + +export default LoyaltyPoint +``` + +You define the `LoyaltyPoint` data model using the `model.define` method of the DML. It accepts the data model's table name as a first parameter, and the model's schema object as a second parameter. + +The `LoyaltyPoint` data model has the following properties: + +- `id`: A unique ID for the loyalty points. +- `points`: The number of loyalty points a customer has. +- `customer_id`: The ID of the customer who owns the loyalty points. This property has a unique index to ensure that each customer has only one record in the `loyalty_point` table. + +Learn more about defining data model properties in the [Property Types documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties/index.html.md). + +### Create Module's Service + +You now have the necessary data model in the Loyalty Module, but you'll need to manage its records. You do this by creating a service in the module. + +A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to the database, allowing you to manage your data models, or connect to a third-party service, which is useful if you're integrating with external services. + +Refer to the [Module Service documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#2-create-service/index.html.md) to learn more. + +To create the Loyalty Module's service, create the file `src/modules/loyalty/service.ts` with the following content: + +```ts title="src/modules/loyalty/service.ts" +import { MedusaError, MedusaService } from "@medusajs/framework/utils" +import LoyaltyPoint from "./models/loyalty-point" +import { InferTypeOf } from "@medusajs/framework/types" + +type LoyaltyPoint = InferTypeOf + +class LoyaltyModuleService extends MedusaService({ + LoyaltyPoint, +}) { + // TODO add methods +} + +export default LoyaltyModuleService +``` + +The `LoyaltyModuleService` extends `MedusaService` from the Modules SDK which generates a class with data-management methods for your module's data models. This saves you time on implementing Create, Read, Update, and Delete (CRUD) methods. + +So, the `LoyaltyModuleService` class now has methods like `createLoyaltyPoints` and `retrieveLoyaltyPoint`. + +Find all methods generated by the `MedusaService` in [the Service Factory reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/index.html.md). + +#### Add Methods to the Service + +Aside from the basic CRUD methods, you need to add methods that handle custom functionalities related to loyalty points. + +First, you need a method that adds loyalty points for a customer. Add the following method to the `LoyaltyModuleService`: + +```ts title="src/modules/loyalty/service.ts" +class LoyaltyModuleService extends MedusaService({ + LoyaltyPoint, +}) { + async addPoints(customerId: string, points: number): Promise { + const existingPoints = await this.listLoyaltyPoints({ + customer_id: customerId, + }) + + if (existingPoints.length > 0) { + return await this.updateLoyaltyPoints({ + id: existingPoints[0].id, + points: existingPoints[0].points + points, + }) + } + + return await this.createLoyaltyPoints({ + customer_id: customerId, + points, + }) + } +} +``` + +You add an `addPoints` method that accepts two parameters: the ID of the customer and the points to add. + +In the method, you retrieve the customer's existing loyalty points using the `listLoyaltyPoints` method, which is automatically generated by the `MedusaService`. If the customer has existing points, you update them with the new points using the `updateLoyaltyPoints` method. + +Otherwise, if the customer doesn't have existing loyalty points, you create a new record with the `createLoyaltyPoints` method. + +The next method you'll add deducts points from the customer's loyalty points, which is useful when the customer redeems points. Add the following method to the `LoyaltyModuleService`: + +```ts title="src/modules/loyalty/service.ts" +class LoyaltyModuleService extends MedusaService({ + LoyaltyPoint, +}) { + // ... + async deductPoints(customerId: string, points: number): Promise { + const existingPoints = await this.listLoyaltyPoints({ + customer_id: customerId, + }) + + if (existingPoints.length === 0 || existingPoints[0].points < points) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "Insufficient loyalty points" + ) + } + + return await this.updateLoyaltyPoints({ + id: existingPoints[0].id, + points: existingPoints[0].points - points, + }) + } +} +``` + +The `deductPoints` method accepts the customer ID and the points to deduct. + +In the method, you retrieve the customer's existing loyalty points using the `listLoyaltyPoints` method. If the customer doesn't have existing points or if the points to deduct are greater than the existing points, you throw an error. + +Otherwise, you update the customer's loyalty points with the new value using the `updateLoyaltyPoints` method, which is automatically generated by `MedusaService`. + +Next, you'll add the method that retrieves the points of a customer. Add the following method to the `LoyaltyModuleService`: + +```ts title="src/modules/loyalty/service.ts" +class LoyaltyModuleService extends MedusaService({ + LoyaltyPoint, +}) { + // ... + async getPoints(customerId: string): Promise { + const points = await this.listLoyaltyPoints({ + customer_id: customerId, + }) + + return points[0]?.points || 0 + } +} +``` + +The `getPoints` method accepts the customer ID and retrieves the customer's loyalty points using the `listLoyaltyPoints` method. If the customer has no points, it returns `0`. + +#### Add Method to Map Points to Discount + +Finally, you'll add a method that implements the logic of mapping loyalty points to a discount amount. This is useful when the customer wants to redeem their points during checkout. + +The mapping logic may differ for each use case. For example, you may need to use a third-party service to map the loyalty points discount amount, or use some custom calculation. + +To simplify the logic in this tutorial, you'll use a simple calculation that maps 1 point to 1 currency unit. For example, `100` points = `$100` discount. + +Add the following method to the `LoyaltyModuleService`: + +```ts title="src/modules/loyalty/service.ts" +class LoyaltyModuleService extends MedusaService({ + LoyaltyPoint, +}) { + // ... + async calculatePointsFromAmount(amount: number): Promise { + // Convert amount to points using a standard conversion rate + // For example, $1 = 1 point + // Round down to nearest whole point + const points = Math.floor(amount) + + if (points < 0) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Amount cannot be negative" + ) + } + + return points + } +} +``` + +The `calculatePointsFromAmount` method accepts the amount and converts it to the nearest whole number of points. If the amount is negative, it throws an error. + +You'll use this method later to calculate the amount discounted when a customer redeems their loyalty points. + +### 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/loyalty/index.ts` with the following content: + +```ts title="src/modules/loyalty/index.ts" +import { Module } from "@medusajs/framework/utils" +import LoyaltyModuleService from "./service" + +export const LOYALTY_MODULE = "loyalty" + +export default Module(LOYALTY_MODULE, { + service: LoyaltyModuleService, +}) +``` + +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 `loyalty`. +2. An object with a required property `service` indicating the module's service. + +You also export the module's name as `LOYALTY_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: "@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, - }, - }, - ], - }, + resolve: "./src/modules/loyalty", }, ], }) ``` -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: +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. -- `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. +### Generate Migrations -Then, set the SendGrid API key and "from" email as environment variables, such as in the `.env` file at the root of your project: +Since data models represent tables in the database, you define how they're created in the database with migrations. A migration is a TypeScript or JavaScript file that defines database changes made by a module. -```plain -SENDGRID_API_KEY=your-sendgrid-api-key -SENDGRID_FROM=test@gmail.com +Refer to the [Migrations documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#5-generate-migrations/index.html.md) to learn more. + +Medusa's CLI tool can generate the migrations for you. To generate a migration for the Loyalty Module, run the following command in your Medusa application's directory: + +```bash +npx medusa db:generate loyalty ``` -You can now use SendGrid to send emails in Medusa. +The `db:generate` command of the Medusa CLI accepts the name of the module to generate the migration for. You'll now have a `migrations` directory under `src/modules/loyalty` that holds the generated migration. + +Then, to reflect these migrations on the database, run the following command: + +```bash +npx medusa db:migrate +``` + +The table for the `LoyaltyPoint` data model is now created in the database. *** -## Step 3: Send Abandoned Cart Notification Flow +## Step 3: Change Loyalty Points Flow -You will now implement the sending logic for the abandoned cart notifications. +Now that you have a module that stores and manages loyalty points in the database, you'll start building flows around it that allow customers to earn and redeem points. -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. +The first flow you'll build will either add points to a customer's loyalty points or deduct them based on a purchased order. If the customer hasn't redeemed points, the points are added to their loyalty points. Otherwise, the points are deducted from their loyalty points. -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. +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's a special function that allows you to track its executions' progress, define roll-back logic, and configure other advanced features. Then, you execute the workflow from other customizations, such as in an endpoint. -The workflow will receive the list of abandoned carts as an input. The workflow has the following steps: +In this section, you'll build the workflow that adds or deducts loyalty points for an order's customer. Later, you'll execute this workflow when an order is placed. -- [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. +Learn more about workflows in the [Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). -Medusa provides the second step in its `@medusajs/medusa/core-flows` package. So, you only need to implement the first one. +The workflow will have the following steps: -### sendAbandonedNotificationsStep +- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the order's details. +- [validateCustomerExistsStep](#validateCustomerExistsStep): Validate that the customer is registered. +- [getCartLoyaltyPromoStep](#getCartLoyaltyPromoStep): Retrieve the cart's loyalty promotion. -The first step of the workflow sends a notification to the owners of the abandoned carts that are passed as an input. +Medusa provides the `useQueryGraphStep` and `updatePromotionsStep` in its `@medusajs/medusa/core-flows` package. So, you'll only implement the other steps. -To implement the step, create the file `src/workflows/steps/send-abandoned-notifications.ts` with the following content: +### validateCustomerExistsStep -```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" +In the workflow, you first need to validate that the customer is registered. Only registered customers can earn and redeem loyalty points. -type SendAbandonedNotificationsStepInput = { - carts: (CartDTO & { - customer: CustomerDTO - })[] +To do this, create the file `src/workflows/steps/validate-customer-exists.ts` with the following content: + +```ts title="src/workflows/steps/validate-customer-exists.ts" +import { CustomerDTO } from "@medusajs/framework/types" +import { createStep } from "@medusajs/framework/workflows-sdk" +import { MedusaError } from "@medusajs/framework/utils" + +export type ValidateCustomerExistsStepInput = { + customer: CustomerDTO | null | undefined } -export const sendAbandonedNotificationsStep = createStep( - "send-abandoned-notifications", - async (input: SendAbandonedNotificationsStepInput, { container }) => { - const notificationModuleService = container.resolve( - Modules.NOTIFICATION - ) +export const validateCustomerExistsStep = createStep( + "validate-customer-exists", + async ({ customer }: ValidateCustomerExistsStepInput) => { + if (!customer) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Customer not found" + ) + } - 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, - }) + if (!customer.has_account) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Customer must have an account to earn or manage points" + ) + } } ) ``` You create a step with `createStep` from the Workflows SDK. It accepts two parameters: -1. The step's unique name, which is `create-review`. +1. The step's unique name, which is `validate-customer-exists`. 2. An async function that receives two parameters: - - The step's input, which is in this case an object with the review's properties. + - The step's input, which is in this case an object with the customer's details. - 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. +In the step function, you validate that the customer is defined and that it's registered based on its `has_account` property. Otherwise, you throw an error. -Notice that each notification is an object with the following properties: +### getCartLoyaltyPromoStep -- `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. +Next, you'll need to retrieve the loyalty promotion applied on the cart, if there's any. This is useful to determine whether the customer has redeemed points. -Based on the dynamic template you create in SendGrid or another provider, you can pass different data in the `data` object. +Before you create a step, you'll create a utility function that the step uses to retrieve the loyalty promotion of a cart. You'll create it as a separate utility function to use it later in other customizations. -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 the file `src/utils/promo.ts` with the following content: -### Create Workflow +```ts title="src/utils/promo.ts" +import { PromotionDTO, CustomerDTO, CartDTO } from "@medusajs/framework/types" -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 type CartData = CartDTO & { + promotions?: PromotionDTO[] + customer?: CustomerDTO + metadata: { + loyalty_promo_id?: string + } } -export const sendAbandonedCartsWorkflow = createWorkflow( - "send-abandoned-carts", - function (input: SendAbandonedCartsWorkflowInput) { - sendAbandonedNotificationsStep(input) +export function getCartLoyaltyPromotion( + cart: CartData +): PromotionDTO | undefined { + if (!cart?.metadata?.loyalty_promo_id) { + return + } - const updateCartsData = transform( - input, - (data) => { - return data.carts.map((cart) => ({ - id: cart.id, - metadata: { - ...cart.metadata, - abandoned_notification: new Date().toISOString(), - }, - })) - } + return cart.promotions?.find( + (promotion) => promotion.id === cart.metadata.loyalty_promo_id + ) +} +``` + +You create a `getCartLoyaltyPromotion` function that accepts the cart's details as an input and returns the loyalty promotion if it exists. You retrieve the loyalty promotion if its ID is stored in the cart's `metadata.loyalty_promo_id` property. + +You can now create the step that uses this utility to retrieve a carts loyalty points promotion. To create the step, create the file `src/workflows/steps/get-cart-loyalty-promo.ts` with the following content: + +```ts title="src/workflows/steps/get-cart-loyalty-promo.ts" +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +import { CartData, getCartLoyaltyPromotion } from "../../utils/promo" +import { MedusaError } from "@medusajs/framework/utils" + +type GetCartLoyaltyPromoStepInput = { + cart: CartData, + throwErrorOn?: "found" | "not-found" +} + +export const getCartLoyaltyPromoStep = createStep( + "get-cart-loyalty-promo", + async ({ cart, throwErrorOn }: GetCartLoyaltyPromoStepInput) => { + const loyaltyPromo = getCartLoyaltyPromotion(cart) + + if (throwErrorOn === "found" && loyaltyPromo) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Loyalty promotion already applied to cart" + ) + } else if (throwErrorOn === "not-found" && !loyaltyPromo) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "No loyalty promotion found on cart" + ) + } + + return new StepResponse(loyaltyPromo) + } +) +``` + +You create a step that accepts an object having the following properties: + +- `cart`: The cart's details. +- `throwErrorOn`: An optional property that indicates whether to throw an error if the loyalty promotion is found or not found. + +The `throwErrorOn` property is useful to make the step reusable in different scenarios, allowing you to use it in later workflows. + +In the step, you call the `getCartLoyaltyPromotion` utility to retrieve the loyalty promotion. If the `throwErrorOn` property is set to `found` and the loyalty promotion is found, you throw an error. + +Otherwise, if the `throwErrorOn` property is set to `not-found` and the loyalty promotion is not found, you throw an error. + +To return data from a step, you return an instance of `StepResponse` from the Workflows SDK. It accepts as a parameter the data to return, which is the loyalty promotion in this case. + +### deductPurchasePointsStep + +If the order's cart has a loyalty promotion, you need to deduct points from the customer's loyalty points. To do this, create the file `src/workflows/steps/deduct-purchase-points.ts` with the following content: + +```ts title="src/workflows/steps/deduct-purchase-points.ts" highlights={deductStepHighlights} collapsibleLines="1-7" expandButtonLabel="Show Imports" +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { LOYALTY_MODULE } from "../../modules/loyalty" +import LoyaltyModuleService from "../../modules/loyalty/service" + +type DeductPurchasePointsInput = { + customer_id: string + amount: number +} + +export const deductPurchasePointsStep = createStep( + "deduct-purchase-points", + async ({ + customer_id, amount, + }: DeductPurchasePointsInput, { container }) => { + const loyaltyModuleService: LoyaltyModuleService = container.resolve( + LOYALTY_MODULE ) - const updatedCarts = updateCartsStep(updateCartsData) + const pointsToDeduct = await loyaltyModuleService.calculatePointsFromAmount( + amount + ) - return new WorkflowResponse(updatedCarts) + const result = await loyaltyModuleService.deductPoints( + customer_id, + pointsToDeduct + ) + + return new StepResponse(result, { + customer_id, + points: pointsToDeduct, + }) + }, + async (data, { container }) => { + if (!data) { + return + } + + const loyaltyModuleService: LoyaltyModuleService = container.resolve( + LOYALTY_MODULE + ) + + // Restore points in case of failure + await loyaltyModuleService.addPoints( + data.customer_id, + data.points + ) + } +) +``` + +You create a step that accepts an object having the following properties: + +- `customer_id`: The ID of the customer to deduct points from. +- `amount`: The promotion's amount, which will be used to calculate the points to deduct. + +In the step, you resolve the Loyalty Module's service from the Medusa container. Then, you use the `calculatePointsFromAmount` method to calculate the points to deduct from the promotion's amount. + +After that, you call the `deductPoints` method to deduct the points from the customer's loyalty points. + +Finally, you return a `StepResponse` with the result of the `deductPoints`. + +#### Compensation Function + +This step has a compensation function, which is passed as a third parameter to the `createStep` 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. + +The compensation function accepts two parameters: + +1. Data passed from the step function to the compensation function. The data is passed as a second parameter of the returned `StepResponse` instance. +2. An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). + +In the compensation function, you resolve the Loyalty Module's service from the Medusa container. Then, you call the `addPoints` method to restore the points deducted from the customer's loyalty points if an error occurs. + +### addPurchaseAsPointsStep + +The last step you'll create adds points to the customer's loyalty points. You'll use this step if the customer didn't redeem points during checkout. + +To create the step, create the file `src/workflows/steps/add-purchase-as-points.ts` with the following content: + +```ts title="src/workflows/steps/add-purchase-as-points.ts" highlights={addPointsHighlights} collapsibleLines="1-7" expandButtonLabel="Show Imports" +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { LOYALTY_MODULE } from "../../modules/loyalty" +import LoyaltyModuleService from "../../modules/loyalty/service" + +type StepInput = { + customer_id: string + amount: number +} + +export const addPurchaseAsPointsStep = createStep( + "add-purchase-as-points", + async (input: StepInput, { container }) => { + const loyaltyModuleService: LoyaltyModuleService = container.resolve( + LOYALTY_MODULE + ) + + const pointsToAdd = await loyaltyModuleService.calculatePointsFromAmount( + input.amount + ) + + const result = await loyaltyModuleService.addPoints( + input.customer_id, + pointsToAdd + ) + + return new StepResponse(result, { + customer_id: input.customer_id, + points: pointsToAdd, + }) + }, + async (data, { container }) => { + if (!data) { + return + } + + const loyaltyModuleService: LoyaltyModuleService = container.resolve( + LOYALTY_MODULE + ) + + await loyaltyModuleService.deductPoints( + data.customer_id, + data.points + ) + } +) +``` + +You create a step that accepts an object having the following properties: + +- `customer_id`: The ID of the customer to add points to. +- `amount`: The order's amount, which will be used to calculate the points to add. + +In the step, you resolve the Loyalty Module's service from the Medusa container. Then, you use the `calculatePointsFromAmount` method to calculate the points to add from the order's amount. + +After that, you call the `addPoints` method to add the points to the customer's loyalty points. + +Finally, you return a `StepResponse` with the result of the `addPoints`. + +You also pass to the compensation function the customer's ID and the points added. In the compensation function, you deduct the points if an error occurs. + +### Add Utility Functions + +Before you create the workflow, you need a utility function that checks whether an order's cart has a loyalty promotion. This is useful to determine whether the customer redeemed points during checkout, allowing you to decide which steps to execute. + +To add the utility function, add the following to `src/utils/promo.ts`: + +```ts title="src/utils/promo.ts" +import { OrderDTO } from "@medusajs/framework/types" + +export type OrderData = OrderDTO & { + promotion?: PromotionDTO[] + customer?: CustomerDTO + cart?: CartData +} + +export const CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE = "customer_id" + +export function orderHasLoyaltyPromotion(order: OrderData): boolean { + const loyaltyPromotion = getCartLoyaltyPromotion( + order.cart as unknown as CartData + ) + + return loyaltyPromotion?.rules?.some((rule) => { + return rule?.attribute === CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE && ( + rule?.values?.some((value) => value.value === order.customer?.id) || false + ) + }) || false +} +``` + +You first define an `OrderData` type that extends the `OrderDTO` type. This type has the order's details, including the cart, customer, and promotions details. + +Then, you define a constant `CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE` that represents the attribute used in the promotion rule to check whether the customer ID is valid. + +Finally, you create the `orderHasLoyaltyPromotion` function that accepts an order's details and checks whether it has a loyalty promotion. It returns `true` if: + +- The order's cart has a loyalty promotion. You use the `getCartLoyaltyPromotion` utility to try to retrieve the loyalty promotion. +- The promotion's rules include the `customer_id` attribute and its value matches the order's customer ID. + - When you create the promotion for the cart later, you'll see how to set this rule. + +You'll use this utility in the workflow next. + +### Create the Workflow + +Now that you have all the steps, you can create the workflow that uses them. + +To create the workflow, create the file `src/workflows/handle-order-points.ts` with the following content: + +```ts title="src/workflows/handle-order-points.ts" highlights={handleOrderPointsHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports" +import { createWorkflow, when } from "@medusajs/framework/workflows-sdk" +import { updatePromotionsStep, useQueryGraphStep } from "@medusajs/medusa/core-flows" +import { validateCustomerExistsStep, ValidateCustomerExistsStepInput } from "./steps/validate-customer-exists" +import { deductPurchasePointsStep } from "./steps/deduct-purchase-points" +import { addPurchaseAsPointsStep } from "./steps/add-purchase-as-points" +import { OrderData, CartData } from "../utils/promo" +import { orderHasLoyaltyPromotion } from "../utils/promo" +import { getCartLoyaltyPromoStep } from "./steps/get-cart-loyalty-promo" + +type WorkflowInput = { + order_id: string +} + +export const handleOrderPointsWorkflow = createWorkflow( + "handle-order-points", + ({ order_id }: WorkflowInput) => { + // @ts-ignore + const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "id", + "customer.*", + "total", + "cart.*", + "cart.promotions.*", + "cart.promotions.rules.*", + "cart.promotions.rules.values.*", + "cart.promotions.application_method.*", + ], + filters: { + id: order_id, + }, + options: { + throwIfKeyNotFound: true, + }, + }) + + validateCustomerExistsStep({ + customer: orders[0].customer, + } as ValidateCustomerExistsStepInput) + + const loyaltyPointsPromotion = getCartLoyaltyPromoStep({ + cart: orders[0].cart as unknown as CartData, + }) + + when(orders, (orders) => + orderHasLoyaltyPromotion(orders[0] as unknown as OrderData) && + loyaltyPointsPromotion !== undefined + ) + .then(() => { + deductPurchasePointsStep({ + customer_id: orders[0].customer!.id, + amount: loyaltyPointsPromotion.application_method!.value as number, + }) + + updatePromotionsStep([ + { + id: loyaltyPointsPromotion.id, + status: "inactive", + }, + ]) + }) + + + when( + orders, + (order) => !orderHasLoyaltyPromotion(order[0] as unknown as OrderData) + ) + .then(() => { + addPurchaseAsPointsStep({ + customer_id: orders[0].customer!.id, + amount: orders[0].total, + }) + }) } ) ``` 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. +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 with the order's ID. 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. +- Use `useQueryGraphStep` to retrieve the order's details. You pass the order's ID as a filter to retrieve the order. + - This step uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), which is a tool that retrieves data across modules. +- Validate that the customer is registered using the `validateCustomerExistsStep`. +- Retrieve the cart's loyalty promotion using the `getCartLoyaltyPromoStep`. +- Use `when` to check whether the order's cart has a loyalty promotion. + - Since you can't perform data manipulation in a workflow's constructor function, `when` allows you to perform steps if a condition is satisfied. + - You pass as a first parameter the object to perform the condition on, which is the order in this case. In the second parameter, you pass a function that returns a boolean value, indicating whether the condition is satisfied. + - To specify the steps to perform if a condition is satisfied, you chain a `then` method to the `when` method. You can perform any step within the `then` method. + - In this case, if the order's cart has a loyalty promotion, you call the `deductPurchasePointsStep` to deduct points from the customer's loyalty points. You also call the `updatePromotionsStep` to deactivate the cart's loyalty promotion. +- You use another `when` to check whether the order's cart doesn't have a loyalty promotion. + - If the condition is satisfied, you call the `addPurchaseAsPointsStep` to add points to the customer's loyalty points. -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). +You'll use this workflow next when an order is placed. -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 -``` +To learn more about the constraints on a workflow's constructor function, refer to the [Workflow Constraints](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md) documentation. Refer to the [When-Then](https://docs.medusajs.com/docs/learn/fundamentals/workflows/conditions/index.html.md) documentation to learn more about the `when` method and how to use it in a workflow. *** -## Step 4: Schedule Cart Abandonment Notifications +## Step 4: Handle Order Placed Event -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. +Now that you have the workflow that handles adding or deducting loyalty points for an order, you need to execute it when an order is placed. -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. +Medusa has an event system that allows you to listen to events emitted by the Medusa server using a [subscriber](https://docs.medusajs.com/docs//learn/fundamentals/events-and-subscribers/index.html.md). A subscriber is an asynchronous function that's executed when its associated event is emitted. In a subscriber, you can execute a workflow that performs actions in result of the event. -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: +In this step, you'll create a subscriber that listens to the `order.placed` event and executes the `handleOrderPointsWorkflow` workflow. -```ts title="src/jobs/send-abandoned-cart-notification.ts" -import { MedusaContainer } from "@medusajs/framework/types" -import { - sendAbandonedCartsWorkflow, - SendAbandonedCartsWorkflowInput, -} from "../workflows/send-abandoned-carts" +Refer to the [Events and Subscribers](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) documentation to learn more. -export default async function abandonedCartJob( - container: MedusaContainer -) { - const logger = container.resolve("logger") - const query = container.resolve("query") +Subscribers are created in a TypeScript or JavaScript file under the `src/subscribers` directory. So, to create a subscriber, create the fle `src/subscribers/order-placed.ts` with the following content: - const oneDayAgo = new Date() - oneDayAgo.setDate(oneDayAgo.getDate() - 1) - const limit = 100 - const offset = 0 - const totalCount = 0 - const abandonedCartsCount = 0 +```ts title="src/subscribers/order-placed.ts" +import type { + SubscriberArgs, + SubscriberConfig, +} from "@medusajs/framework" +import { handleOrderPointsWorkflow } from "../workflows/handle-order-points" - 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({ +export default async function orderPlacedHandler({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + await handleOrderPointsWorkflow(container).run({ input: { - carts: cartsWithItems, - } as unknown as SendAbandonedCartsWorkflowInput, + order_id: data.id, + }, }) - abandonedCartsCount += cartsWithItems.length - -} catch (error) { - logger.error( - `Failed to send abandoned cart notification: ${error.message}` - ) } -offset += limit +export const config: SubscriberConfig = { + event: "order.placed", +} ``` -In the do-while loop, you use Query to retrieve carts matching the following criteria: +The subscriber file must export: -- The cart was last updated more than a day ago. -- The cart has an email address. -- The cart has not been completed. +- An asynchronous subscriber function that's executed whenever the associated event, which is `order.placed` is triggered. +- A configuration object with an event property whose value is the event the subscriber is listening to. You can also pass an array of event names to listen to multiple events in the same subscriber. -You also filter the retrieved carts to only include carts with items and customers that have not received an abandoned cart notification. +The subscriber function accepts an object with the following properties: -Finally, you execute the `sendAbandonedCartsWorkflow` passing it the abandoned carts as an input. You will execute the workflow for each paginated batch of carts. +- `event`: An object with the event's data payload. For example, the `order.placed` event has the order's ID in its data payload. +- `container`: The Medusa container, which you can use to resolve services and tools. + +In the subscriber function, you execute the `handleOrderPointsWorkflow` by invoking it, passing it the Medusa container, then using its `run` method, passing it the workflow's input. + +Whenever an order is placed now, the subscriber will be executed, which in turn will execute the workflow that handles the loyalty points flow. ### 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: +To test out the loyalty points flow, you'll use the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) that you installed in the first step. As mentioned in that step, the storefront will be installed in a separate directory from the Medusa application, and its name is `{project-name}-storefront`, where `{project-name}` is the name of your Medusa application's directory. -```ts title="src/jobs/send-abandoned-cart-notification.ts" -oneDayAgo.setMinutes(oneDayAgo.getMinutes() - 1) // For testing -``` +So, run the following command in the Medusa application's directory to start the Medusa server: -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 +```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green" 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: +Then, run the following command in the Next.js Starter Storefront's directory to start the Next.js server: -```bash npm2yarn +```bash npm2yarn badgeLabel="Storefront" badgeColor="blue" npm run dev ``` -Open the storefront at `localhost:8000`. You can either: +The Next.js Starter Storefront will be running on `http://localhost:8000`, and the Medusa server will be running on `http://localhost:9000`. -- 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. +Open the Next.js Starter Storefront in your browser and create a new account by going to Account at the top right. -Afterwards, wait for the job to execute. Once it is executed, you will see the following message in the terminal: +Once you're logged in, add an item to the cart and go through the checkout flow. + +After you place the order, you'll see the following message in your Medusa application's terminal: ```bash -info: Sent 1 abandoned cart notifications +info: Processing order.placed which has 1 subscribers ``` -Once you're done testing, make sure to revert the changes to the `oneDayAgo` date and the job's schedule. +This message indicates that the `order.placed` event was emitted, and that your subscriber was executed. + +Since you didn't redeem any points during checkout, loyalty points will be added to your account. You'll implement an API route that allows you to retrieve the loyalty points in the next step. *** -## Step 5: Recover Cart in Storefront +## Step 5: Retrieve Loyalty Points API Route -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. +Next, you want to allow customers to view their loyalty points. You can show them on their profile page, or during checkout. -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: +To expose a feature to clients, you create 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. -```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 -}> +You'll create an API route at the path `/store/customers/me/loyalty-points` that returns the loyalty points of the authenticated customer. -export async function GET(req: NextRequest, { params }: { params: Params }) { - const { id } = await params - const cart = await retrieveCart(id) +Learn more about API routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). - if (!cart) { - return notFound() - } +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`. - setCartId(id) +So, to create an API route at the path `/store/customers/me/loyalty-points`, create the file `src/api/store/customers/me/loyalty-points/route.ts` with the following content: - const countryCode = cart.shipping_address?.country_code || - cart.region?.countries?.[0]?.iso_2 +```ts title="src/api/store/customers/me/loyalty-points/route.ts" - redirect( - `/${countryCode ? `${countryCode}/` : ""}cart` +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { LOYALTY_MODULE } from "../../../../../modules/loyalty" +import LoyaltyModuleService from "../../../../../modules/loyalty/service" + +export async function GET( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) { + const loyaltyModuleService: LoyaltyModuleService = req.scope.resolve( + LOYALTY_MODULE ) + + const points = await loyaltyModuleService.getPoints( + req.auth_context.actor_id + ) + + res.json({ + points, + }) } ``` -You add a `GET` route handler that receives the cart ID as a path parameter. In the route handler, you: +Since you export a `GET` route handler function, you're exposing a `GET` endpoint at `/store/customers/me/loyalty-points`. The route handler function accepts two parameters: -- 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. +1. A request object with details and context on the request, such as body parameters or authenticated customer details. +2. A response object to manipulate and send the response. + +In the route handler, you resolve the Loyalty Module's service from the Medusa container (which is available at `req.scope`). + +Then, you call the service's `getPoints` method to retrieve the authenticated customer's loyalty points. Note that routes starting with `/store/customers/me` are only accessible by authenticated customers. You can access the authenticated customer ID from the request's context, which is available at `req.auth_context.actor_id`. + +Finally, you return the loyalty points in the response. + +You'll test out this route as you customize the Next.js Starter Storefront next. + +*** + +## Step 6: Show Loyalty Points During Checkout + +Now that you have the API route to retrieve the loyalty points, you can show them during checkout. + +In this step, you'll customize the Next.js Starter Storefront to show the loyalty points in the checkout page. + +First, you'll add a server action function that retrieves the loyalty points from the route you created earlier. In `src/lib/data/customer.ts`, add the following function: + +```ts title="src/lib/data/customer.ts" badgeLabel="Storefront" badgeColor="blue" +export const getLoyaltyPoints = async () => { + const headers = { + ...(await getAuthHeaders()), + } + + return sdk.client.fetch<{ points: number }>( + `/store/customers/me/loyalty-points`, + { + method: "GET", + headers, + } + ) + .then(({ points }) => points) + .catch(() => null) +} +``` + +You add a `getLoyaltyPoints` function that retrieves the authenticated customer's loyalty points from the API route you created earlier. You pass the authentication headers using the `getAuthHeaders` function, which is a utility function defined in the Next.js Starter Storefront. + +If the customer isn't authenticated, the request will fail. So, you catch the error and return `null` in that case. + +Next, you'll create a component that shows the loyalty points in the checkout page. Create the file `src/modules/checkout/components/loyalty-points/index.tsx` with the following content: + +```tsx title="src/modules/checkout/components/loyalty-points/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={loyaltyPointsHighlights} +"use client" + +import { HttpTypes } from "@medusajs/types" +import { useEffect, useMemo, useState } from "react" +import { getLoyaltyPoints } from "../../../../lib/data/customer" +import { Button, Heading } from "@medusajs/ui" +import Link from "next/link" + +type LoyaltyPointsProps = { + cart: HttpTypes.StoreCart & { + promotions: HttpTypes.StorePromotion[] + } +} + +const LoyaltyPoints = ({ cart }: LoyaltyPointsProps) => { + const isLoyaltyPointsPromoApplied = useMemo(() => { + return cart.promotions.find( + (promo) => promo.id === cart.metadata?.loyalty_promo_id + ) !== undefined + }, [cart]) + const [loyaltyPoints, setLoyaltyPoints] = useState< + number | null + >(null) + + useEffect(() => { + getLoyaltyPoints() + .then((points) => { + console.log(points) + setLoyaltyPoints(points) + }) + }, []) + + const handleTogglePromotion = async ( + e: React.MouseEvent + ) => { + e.preventDefault() + // TODO apply or remove loyalty promotion + } + + return ( + <> +
+
+ + Loyalty Points + + {loyaltyPoints === null && ( + + Sign up to get and use loyalty points + + )} + {loyaltyPoints !== null && ( +
+ + + You have {loyaltyPoints} loyalty points + +
+ )} +
+ + ) +} + +export default LoyaltyPoints +``` + +You create a `LoyaltyPoints` component that accepts the cart's details as a prop. In the component, you: + +- Create a `isLoyaltyPointsPromoApplied` memoized value that checks whether the cart has a loyalty promotion applied. You use the `cart.metadata.loyalty_promo_id` property to check this. +- Create a `loyaltyPoints` state to store the customer's loyalty points. +- Call the `getLoyaltyPoints` function in a `useEffect` hook to retrieve the loyalty points from the API route you created earlier. You set the `loyaltyPoints` state with the retrieved points. +- Define `handleTogglePromotion` that, when clicked, would either apply or remove the promotion. You'll implement these functionalities later. +- Render the loyalty points in the component. If the customer isn't authenticated, you show a link to the account page to sign up. Otherwise, you show the loyalty points and a button to apply or remove the promotion. + +Next, you'll show this component at the end of the checkout's summary component. So, import the component in `src/modules/checkout/templates/checkout-summary/index.tsx`: + +```tsx title="src/modules/checkout/templates/checkout-summary/index.tsx" badgeLabel="Storefront" badgeColor="blue" +import LoyaltyPoints from "../../components/loyalty-points" +``` + +Then, in the return statement of the `CheckoutSummary` component, add the following after the `div` wrapping the `DiscountCode`: + +```tsx title="src/modules/checkout/templates/checkout-summary/index.tsx" badgeLabel="Storefront" badgeColor="blue" + +``` + +This will show the loyalty points component at the end of the checkout summary. ### Test it Out -To test it out, start the Medusa application: +To test out the customizations to the checkout flow, make sure both the Medusa application and Next.js Starter Storefront are running. -```bash npm2yarn -npm run dev +Then, as an authenticated customer, add an item to cart and proceed to checkout. You'll find a new "Loyalty Points" section at the end of the checkout summary. + +![Loyalty Points Section at the end of the summary section at the right](https://res.cloudinary.com/dza7lstvk/image/upload/v1744195223/Medusa%20Resources/Screenshot_2025-04-09_at_1.39.34_PM_l5oltc.png) + +If you made a purchase before, you can see your loyalty points. You'll also see the "Apply Loyalty Points" button, which doesn't yet do anything. You'll add the functionality next. + +*** + +## Step 7: Apply Loyalty Points to Cart + +The next feature you'll implement allows the customer to apply their loyalty points during checkout. To implement the feature, you need: + +- A workflow that implements the steps of the apply loyalty points flow. +- An API route that exposes the workflow's functionality to clients. You'll then send a request to this API route to apply the loyalty points on the customer's cart. +- A function in the Next.js Starter Storefront that sends the request to the API route you created earlier. + +The workflow will have the following steps: + +- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details. +- [validateCustomerExistsStep](#validateCustomerExistsStep): Validate that the customer is registered. +- [getCartLoyaltyPromoStep](#getCartLoyaltyPromoStep): Retrieve the cart's loyalty promotion. +- [getCartLoyaltyPromoAmountStep](#getCartLoyaltyPromoAmountStep): Get the amount to be discounted based on the loyalty points. +- [createPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPromotionsStep/index.html.md): Create a new loyalty promotion for the cart. +- [updateCartPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/workflows/updateCartPromotionsWorkflow/index.html.md): Update the cart's promotions with the new loyalty promotion. +- [updateCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartsStep/index.html.md): Update the cart to store the ID of the loyalty promotion in the metadata. +- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details again. + +Most of the workflow's steps are either provided by Medusa in the `@medusajs/medusa/core-flows` package or steps you've already implemented. You only need to implement the `getCartLoyaltyPromoAmountStep` step. + +### getCartLoyaltyPromoAmountStep + +The fourth step in the workflow is the `getCartLoyaltyPromoAmountStep`, which retrieves the amount to be discounted based on the loyalty points. This step is useful to determine how much discount to apply to the cart. + +To create the step, create the file `src/workflows/steps/get-cart-loyalty-promo-amount.ts` with the following content: + +```ts title="src/workflows/steps/get-cart-loyalty-promo-amount.ts" highlights={getCartLoyaltyPromoAmountStepHighlights} +import { PromotionDTO, CustomerDTO } from "@medusajs/framework/types" +import { MedusaError } from "@medusajs/framework/utils" +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +import LoyaltyModuleService from "../../modules/loyalty/service" +import { LOYALTY_MODULE } from "../../modules/loyalty" + +export type GetCartLoyaltyPromoAmountStepInput = { + cart: { + id: string + customer: CustomerDTO + promotions?: PromotionDTO[] + total: number + } +} + +export const getCartLoyaltyPromoAmountStep = createStep( + "get-cart-loyalty-promo-amount", + async ({ cart }: GetCartLoyaltyPromoAmountStepInput, { container }) => { + // Check if customer has any loyalty points + const loyaltyModuleService: LoyaltyModuleService = container.resolve( + LOYALTY_MODULE + ) + const loyaltyPoints = await loyaltyModuleService.getPoints( + cart.customer.id + ) + + if (loyaltyPoints <= 0) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Customer has no loyalty points" + ) + } + + const pointsAmount = await loyaltyModuleService.calculatePointsFromAmount( + loyaltyPoints + ) + + const amount = Math.min(pointsAmount, cart.total) + + return new StepResponse(amount) + } +) ``` -And in the Next.js Starter Storefront's directory, start the storefront: +You create a step that accepts an object having the cart's details. -```bash npm2yarn -npm run dev +In the step, you resolve the Loyalty Module's service from the Medusa container. Then, you call the `getPoints` method to retrieve the customer's loyalty points. If the customer has no loyalty points, you throw an error. + +Next, you call the `calculatePointsFromAmount` method to calculate the amount to be discounted based on the loyalty points. You use the `Math.min` function to ensure that the amount doesn't exceed the cart's total. + +Finally, you return a `StepResponse` with the amount to be discounted. + +### Create the Workflow + +You can now create the workflow that applies a loyalty promotion to the cart. + +To create the workflow, create the file `src/workflows/apply-loyalty-on-cart.ts` with the following content: + +```ts title="src/workflows/apply-loyalty-on-cart.ts" highlights={applyLoyaltyOnCartWorkflowHighlights} collapsibleLines="1-24" expandButtonLabel="Show Imports" +import { + createWorkflow, + transform, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { + createPromotionsStep, + updateCartPromotionsWorkflow, + updateCartsStep, + useQueryGraphStep, +} from "@medusajs/medusa/core-flows" +import { + validateCustomerExistsStep, + ValidateCustomerExistsStepInput, +} from "./steps/validate-customer-exists" +import { + getCartLoyaltyPromoAmountStep, + GetCartLoyaltyPromoAmountStepInput, +} from "./steps/get-cart-loyalty-promo-amount" +import { CartData, CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE } from "../utils/promo" +import { CreatePromotionDTO } from "@medusajs/framework/types" +import { PromotionActions } from "@medusajs/framework/utils" +import { getCartLoyaltyPromoStep } from "./steps/get-cart-loyalty-promo" + +type WorkflowInput = { + cart_id: string +} + +const fields = [ + "id", + "customer.*", + "promotions.*", + "promotions.application_method.*", + "promotions.rules.*", + "promotions.rules.values.*", + "currency_code", + "total", + "metadata", +] + +export const applyLoyaltyOnCartWorkflow = createWorkflow( + "apply-loyalty-on-cart", + (input: WorkflowInput) => { + // @ts-ignore + const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields, + filters: { + id: input.cart_id, + }, + options: { + throwIfKeyNotFound: true, + }, + }) + + validateCustomerExistsStep({ + customer: carts[0].customer, + } as ValidateCustomerExistsStepInput) + + getCartLoyaltyPromoStep({ + cart: carts[0] as unknown as CartData, + throwErrorOn: "found", + }) + + const amount = getCartLoyaltyPromoAmountStep({ + cart: carts[0], + } as unknown as GetCartLoyaltyPromoAmountStepInput) + + // TODO create and apply the promotion on the cart + } +) ``` -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. +You create a workflow that accepts an object with the cart's ID as input. -![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) +So far, you: + +- Use `useQueryGraphStep` to retrieve the cart's details. You pass the cart's ID as a filter to retrieve the cart. +- Validate that the customer is registered using the `validateCustomerExistsStep`. +- Check whether the cart has a loyalty promotion using the `getCartLoyaltyPromoStep`. You pass the `throwErrorOn` parameter with the value `found` to throw an error if a loyalty promotion is found in the cart. +- Retrieve the amount to be discounted based on the loyalty points using the `getCartLoyaltyPromoAmountStep`. + +Next, you need to create a new loyalty promotion for the cart. First, you'll prepare the data of the promotion to be created. + +Replace the `TODO` with the following: + +```ts title="src/workflows/apply-loyalty-on-cart.ts" highlights={prepareLoyaltyPromoDataHighlights} +const promoToCreate = transform({ + carts, + amount, +}, (data) => { + const randomStr = Math.random().toString(36).substring(2, 8) + const uniqueId = ( + "LOYALTY-" + data.carts[0].customer?.first_name + "-" + randomStr + ).toUpperCase() + return { + code: uniqueId, + type: "standard", + status: "active", + application_method: { + type: "fixed", + value: data.amount, + target_type: "order", + currency_code: data.carts[0].currency_code, + allocation: "across", + }, + rules: [ + { + attribute: CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE, + operator: "eq", + values: [data.carts[0].customer!.id], + }, + ], + campaign: { + name: uniqueId, + description: "Loyalty points promotion for " + data.carts[0].customer!.email, + campaign_identifier: uniqueId, + budget: { + type: "usage", + limit: 1, + }, + }, + } +}) + +// TODO create promotion and apply it on cart +``` + +Since data manipulation isn't allowed in a workflow constructor, you use the [transform](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) function from the Workflows SDK. It accepts two parameters: + +- The data to perform manipulation on. In this case, you pass the cart's details and the amount to be discounted. +- A function that receives the data from the first parameter, and returns the transformed data. + +In the transformation function, you prepare th data of the loyalty promotion to be created. Some key details include: + +- You set the discount amount in the application method of the promotion. +- You add a rule to the promotion that ensures it can be used only in carts having their `customer_id` equal to this customer's ID. This prevents other customers from using this promotion. +- You create a campaign for the promotion, and you set the campaign budget to a single usage. This prevents the customer from using the promotion again. + +Learn more about promotion concepts in the [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md)'s documentation. + +You can now use the returned data to create a promotion and apply it to the cart. Replace the new `TODO` with the following: + +```ts title="src/workflows/apply-loyalty-on-cart.ts" highlights={createLoyaltyPromoStepHighlights} +const loyaltyPromo = createPromotionsStep([ + promoToCreate, +] as CreatePromotionDTO[]) + +const { metadata, ...updatePromoData } = transform({ + carts, + promoToCreate, + loyaltyPromo, +}, (data) => { + const promos = [ + ...(data.carts[0].promotions?.map((promo) => promo?.code).filter(Boolean) || []) as string[], + data.promoToCreate.code, + ] + + return { + cart_id: data.carts[0].id, + promo_codes: promos, + action: PromotionActions.ADD, + metadata: { + loyalty_promo_id: data.loyaltyPromo[0].id, + }, + } +}) + +updateCartPromotionsWorkflow.runAsStep({ + input: updatePromoData, +}) + +updateCartsStep([ + { + id: input.cart_id, + metadata, + }, +]) + +// retrieve cart with updated promotions +// @ts-ignore +const { data: updatedCarts } = useQueryGraphStep({ + entity: "cart", + fields, + filters: { id: input.cart_id }, +}).config({ name: "retrieve-cart" }) + +return new WorkflowResponse(updatedCarts[0]) +``` + +In the rest of the workflow, you: + +- Create the loyalty promotion using the data you prepared earlier using the `createPromotionsStep`. +- Use the `transform` function to prepare the data to update the cart's promotions. You add the new loyalty promotion code to the cart's promotions codes, and set the `loyalty_promo_id` in the cart's metadata. +- Update the cart's promotions with the new loyalty promotion using the `updateCartPromotionsWorkflow` workflow. +- Update the cart's metadata with the loyalty promotion ID using the `updateCartsStep`. +- Retrieve the cart's details again using `useQueryGraphStep` to get the updated cart with the new loyalty promotion. + +To return data from the workflow, you must return an instance of `WorkflowResponse`. You pass it the data to be returned, which is in this case the cart's details. + +### Create the API Route + +Next, you'll create the API route that executes this workflow. + +To create the API route, create the file `src/api/store/carts/[id]/loyalty-points/route.ts` with the following content: + +```ts title="src/api/store/carts/[id]/loyalty-points/route.ts" +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +import { applyLoyaltyOnCartWorkflow } from "../../../../../workflows/apply-loyalty-on-cart" + +export async function POST( + req: MedusaRequest, + res: MedusaResponse +) { + const { id: cart_id } = req.params + + const { result: cart } = await applyLoyaltyOnCartWorkflow(req.scope) + .run({ + input: { + cart_id, + }, + }) + + res.json({ cart }) +} +``` + +Since you export a `POST` route handler, you expose a `POST` API route at `/store/carts/[id]/loyalty-points`. + +In the route handler, you execute the `applyLoyaltyOnCartWorkflow` workflow, passing it the cart ID as an input. You return the cart's details in the response. + +You can now use this API route in the Next.js Starter Storefront. + +### Apply Loyalty Points in the Storefront + +In the Next.js Starter Storefront, you need to add a server action function that sends a request to the API route you created earlier. Then, you'll use that function when the customer clicks the "Apply Loyalty Points" button. + +To add the function, add the following to `src/lib/data/cart.ts` in the Next.js Starter Storefront: + +```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="blue" +export async function applyLoyaltyPointsOnCart() { + const cartId = await getCartId() + const headers = { + ...(await getAuthHeaders()), + } + + return await sdk.client.fetch<{ + cart: HttpTypes.StoreCart & { + promotions: HttpTypes.StorePromotion[] + } + }>(`/store/carts/${cartId}/loyalty-points`, { + method: "POST", + headers, + }) + .then(async (result) => { + const cartCacheTag = await getCacheTag("carts") + revalidateTag(cartCacheTag) + + return result + }) +} +``` + +You create an `applyLoyaltyPointsOnCart` function that sends a request to the API route you created earlier. + +In the function, you retrieve the cart ID stored in the cookie using the `getCartId` function, which is available in the Next.js Starter Storefront. + +Then, you send the request. Once the request is resolved successfully, you revalidate the cart cache tag to ensure that the cart's details are updated and refetched by other components. This ensures that the applied promotion is shown in the checkout summary without needing to refresh the page. + +Finally, you'll use this function in the `handleTogglePromotion` function in the `LoyaltyPoints` component you created earlier. + +At the top of `src/modules/checkout/components/loyalty-points/index.tsx`, import the function: + +```tsx title="src/modules/checkout/components/loyalty-points/index.tsx" badgeLabel="Storefront" badgeColor="blue" +import { applyLoyaltyPointsOnCart } from "../../../../lib/data/cart" +``` + +Then, replace the `handleTogglePromotion` function with the following: + +```tsx title="src/modules/checkout/components/loyalty-points/index.tsx" badgeLabel="Storefront" badgeColor="blue" +const handleTogglePromotion = async ( + e: React.MouseEvent +) => { + e.preventDefault() + if (!isLoyaltyPointsPromoApplied) { + await applyLoyaltyPointsOnCart() + } else { + // TODO remove loyalty points + } +} +``` + +In the `handleTogglePromotion` function, you call the `applyLoyaltyPointsOnCart` function if the cart doesn't have a loyalty promotion. This will send a request to the API route you created earlier, which will execute the workflow that applies the loyalty promotion to the cart. + +You'll implement removing the loyalty points promotion in a later step. + +### Test it Out + +To test out applying the loyalty points on the cart, start the Medusa application and Next.js Starter Storefront. + +Then, in the checkout flow as an authenticated customer, click on the "Apply Loyalty Points" button. The checkout summary will be updated with the applied promotion and the discount amount. + +If you don't want the promotion to be shown in the "Promotions(s) applied" section, you can filter the promotions in `src/modules/checkout/components/discount-code/index.tsx` to not show a promotion matching `cart.metadata.loyalty_promo_id`. + +![Discounted amount is shown as part of the summary and the promotion is shown as part of the applied promotions](https://res.cloudinary.com/dza7lstvk/image/upload/v1744200895/Medusa%20Resources/Screenshot_2025-04-09_at_3.14.19_PM_abmtjh.png) + +*** + +## Step 8: Remove Loyalty Points From Cart + +In this step, you'll implement the functionality to remove the loyalty points promotion from the cart. This is useful if the customer changes their mind and wants to remove the promotion. + +To implement this functionality, you'll need to: + +- Create a workflow that removes the loyalty points promotion from the cart. +- Create an API route that executes the workflow. +- Create a function in the Next.js Starter Storefront that sends a request to the API route you created earlier. +- Use the function in the `handleTogglePromotion` function in the `LoyaltyPoints` component you created earlier. + +### Create the Workflow + +The workflow will have the following steps: + +- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details. +- [getCartLoyaltyPromoStep](#getCartLoyaltyPromoStep): Retrieve the cart's loyalty promotion. +- [updateCartPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/workflows/updateCartPromotionsWorkflow/index.html.md): Update the cart's promotions to remove the loyalty promotion. +- [updateCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartsStep/index.html.md): Update the cart to remove the loyalty promotion ID from the metadata. +- [updatePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePromotionsStep/index.html.md): Deactive the loyalty promotion. +- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details again. + +Since you already have all the steps, you can create the workflow. + +To create the workflow, create the file `src/workflows/remove-loyalty-from-cart.ts` with the following content: + +```ts title="src/workflows/remove-loyalty-from-cart.ts" collapsibleLines="1-15" expandButtonLabel="Show Imports" highlights={removeLoyaltyFromCartWorkflowHighlights} +import { + createWorkflow, + transform, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { + useQueryGraphStep, + updateCartPromotionsWorkflow, + updateCartsStep, + updatePromotionsStep, +} from "@medusajs/medusa/core-flows" +import { getCartLoyaltyPromoStep } from "./steps/get-cart-loyalty-promo" +import { PromotionActions } from "@medusajs/framework/utils" +import { CartData } from "../utils/promo" + +type WorkflowInput = { + cart_id: string +} + +const fields = [ + "id", + "customer.*", + "promotions.*", + "promotions.application_method.*", + "promotions.rules.*", + "promotions.rules.values.*", + "currency_code", + "total", + "metadata", +] + +export const removeLoyaltyFromCartWorkflow = createWorkflow( + "remove-loyalty-from-cart", + (input: WorkflowInput) => { + // @ts-ignore + const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields, + filters: { + id: input.cart_id, + }, + }) + + const loyaltyPromo = getCartLoyaltyPromoStep({ + cart: carts[0] as unknown as CartData, + throwErrorOn: "not-found", + }) + + updateCartPromotionsWorkflow.runAsStep({ + input: { + cart_id: input.cart_id, + promo_codes: [loyaltyPromo.code!], + action: PromotionActions.REMOVE, + }, + }) + + const newMetadata = transform({ + carts, + }, (data) => { + const { loyalty_promo_id, ...rest } = data.carts[0].metadata || {} + + return { + ...rest, + loyalty_promo_id: null, + } + }) + + updateCartsStep([ + { + id: input.cart_id, + metadata: newMetadata, + }, + ]) + + updatePromotionsStep([ + { + id: loyaltyPromo.id, + status: "inactive", + }, + ]) + + // retrieve cart with updated promotions + // @ts-ignore + const { data: updatedCarts } = useQueryGraphStep({ + entity: "cart", + fields, + filters: { id: input.cart_id }, + }).config({ name: "retrieve-cart" }) + + return new WorkflowResponse(updatedCarts[0]) + } +) +``` + +You create a workflow that accepts an object with the cart's ID as input. + +In the workflow, you: + +- Use `useQueryGraphStep` to retrieve the cart's details. You pass the cart's ID as a filter to retrieve the cart. +- Check whether the cart has a loyalty promotion using the `getCartLoyaltyPromoStep`. You pass the `throwErrorOn` parameter with the value `not-found` to throw an error if a loyalty promotion isn't found in the cart. +- Update the cart's promotions using the `updateCartPromotionsWorkflow`, removing the loyalty promotion. +- Use the `transform` function to prepare the new metadata of the cart. You remove the `loyalty_promo_id` from the metadata. +- Update the cart's metadata with the new metadata using the `updateCartsStep`. +- Deactivate the loyalty promotion using the `updatePromotionsStep`. +- Retrieve the cart's details again using `useQueryGraphStep` to get the updated cart with the new loyalty promotion. +- Return the cart's details in a `WorkflowResponse` instance. + +### Create the API Route + +Next, you'll create the API route that executes this workflow. + +To create the API route, add the following in `src/api/store/carts/[id]/loyalty-points/route.ts`: + +```ts title="src/api/store/carts/[id]/loyalty-points/route.ts" +// other imports... +import { removeLoyaltyFromCartWorkflow } from "../../../../../workflows/remove-loyalty-from-cart" + +// ... +export async function DELETE( + req: MedusaRequest, + res: MedusaResponse +) { + const { id: cart_id } = req.params + + const { result: cart } = await removeLoyaltyFromCartWorkflow(req.scope) + .run({ + input: { + cart_id, + }, + }) + + res.json({ cart }) +} +``` + +You export a `DELETE` route handler, which exposes a `DELETE` API route at `/store/carts/[id]/loyalty-points`. + +In the route handler, you execute the `removeLoyaltyFromCartWorkflow` workflow, passing it the cart ID as an input. You return the cart's details in the response. + +You can now use this API route in the Next.js Starter Storefront. + +### Remove Loyalty Points in the Storefront + +In the Next.js Starter Storefront, you need to add a server action function that sends a request to the API route you created earlier. Then, you'll use that function when the customer clicks the "Remove Loyalty Points" button, which shows when the cart has a loyalty promotion applied. + +To add the function, add the following to `src/lib/data/cart.ts`: + +```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="blue" +export async function removeLoyaltyPointsOnCart() { + const cartId = await getCartId() + const headers = { + ...(await getAuthHeaders()), + } + const next = { + ...(await getCacheOptions("carts")), + } + + return await sdk.client.fetch<{ + cart: HttpTypes.StoreCart & { + promotions: HttpTypes.StorePromotion[] + } + }>(`/store/carts/${cartId}/loyalty-points`, { + method: "DELETE", + headers, + }) + .then(async (result) => { + const cartCacheTag = await getCacheTag("carts") + revalidateTag(cartCacheTag) + + return result + }) +} +``` + +You create a `removeLoyaltyPointsOnCart` function that sends a request to the API route you created earlier. + +In the function, you retrieve the cart ID stored in the cookie using the `getCartId` function, which is available in the Next.js Starter Storefront. + +Then, you send the request to the API route. Once the request is resolved successfully, you revalidate the cart cache tag to ensure that the cart's details are updated and refetched by other components. This ensures that the promotion is removed from the checkout summary without needing to refresh the page. + +Finally, you'll use this function in the `handleTogglePromotion` function in the `LoyaltyPoints` component you created earlier. + +At the top of `src/modules/checkout/components/loyalty-points/index.tsx`, add the following import: + +```tsx title="src/modules/checkout/components/loyalty-points/index.tsx" badgeLabel="Storefront" badgeColor="blue" +import { removeLoyaltyPointsOnCart } from "../../../../lib/data/cart" +``` + +Then, replace the `TODO` in `handleTogglePromotion` with the following: + +```tsx title="src/modules/checkout/components/loyalty-points/index.tsx" badgeLabel="Storefront" badgeColor="blue" +await removeLoyaltyPointsOnCart() +``` + +In the `handleTogglePromotion` function, you call the `removeLoyaltyPointsOnCart` function if the cart has a loyalty promotion. This will send a request to the API route you created earlier, which will execute the workflow that removes the loyalty promotion from the cart. + +### Test it Out + +To test out removing the loyalty points from the cart, start the Medusa application and Next.js Starter Storefront. + +Then, in the checkout flow as an authenticated customer, after applying the loyalty points, click on the "Remove Loyalty Points" button. The checkout summary will be updated with the removed promotion and the discount amount. + +![The "Remove Loyalty Points" button is shown in the "Loyalty Points" section](https://res.cloudinary.com/dza7lstvk/image/upload/v1744204436/Medusa%20Resources/Screenshot_2025-04-09_at_4.13.24_PM_xt5trh.png) + +*** + +## Step 9: Validate Loyalty Points on Cart Completion + +After the customer applies the loyalty points to the cart and places the order, you need to validate that the customer actually has the loyalty points. This prevents edge cases where the customer may have applied the loyalty points previously but they don't have them anymore. + +So, in this step, you'll hook into Medusa's cart completion flow to perform the validation. + +Since Medusa uses workflows in its API routes, it allows you to hook into them and perform custom functionalities using [Workflow Hooks](https://docs.medusajs.com/docs/learn/fundamentals/workflows/workflow-hooks/index.html.md). A workflow hook is a point in a workflow where you can inject custom functionality as a step function, called a hook handler. + +Medusa uses the [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) hook to complete the cart and place an order. This workflow has a `validate` hook that allows you to perform custom validation before the cart is completed. + +To consume the `validate` hook, create the file `src/workflows/hooks/complete-cart.ts` with the following content: + +```ts title="src/workflows/hooks/complete-cart.ts" highlights={completeCartWorkflowHookHighlights} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import { completeCartWorkflow } from "@medusajs/medusa/core-flows" +import LoyaltyModuleService from "../../modules/loyalty/service" +import { LOYALTY_MODULE } from "../../modules/loyalty" +import { CartData, getCartLoyaltyPromotion } from "../../utils/promo" +import { MedusaError } from "@medusajs/framework/utils" + +completeCartWorkflow.hooks.validate( + async ({ cart }, { container }) => { + const query = container.resolve("query") + const loyaltyModuleService: LoyaltyModuleService = container.resolve( + LOYALTY_MODULE + ) + + const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "id", + "promotions.*", + "customer.*", + "promotions.rules.*", + "promotions.rules.values.*", + "promotions.application_method.*", + "metadata", + ], + filters: { + id: cart.id, + }, + }, { + throwIfKeyNotFound: true, + }) + + const loyaltyPromo = getCartLoyaltyPromotion( + carts[0] as unknown as CartData + ) + + if (!loyaltyPromo) { + return + } + + const customerLoyaltyPoints = await loyaltyModuleService.getPoints( + carts[0].customer!.id + ) + const requiredPoints = await loyaltyModuleService.calculatePointsFromAmount( + loyaltyPromo.application_method!.value as number + ) + + if (customerLoyaltyPoints < requiredPoints) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Customer does not have enough loyalty points. Required: ${ + requiredPoints + }, Available: ${customerLoyaltyPoints}` + ) + } + } +) +``` + +Workflows have a special `hooks` property that includes all the hooks tht you can consume in that workflow. You consume the hook by invoking it from the workflow's `hooks` property. + +Since the hook is essentially a step function, it accepts the following parameters: + +- The hook's input passed from the workflow, which differs for each hook. The `validate` hook receives an object having the cart's details. +- The step context object, which contains the Medusa container. You can use it to resolve services and perform actions. + +In the hook, you resolve Query and the Loyalty Module's service. Then, you use Query to retrieve the cart's necessary details, including its promotions, customer, and metadata. + +After that, you retrieve the customer's loyalty points and calculate the required points to apply the loyalty promotion. + +If the customer doesn't have enough loyalty points, you throw an error. This will prevent the cart from being completed if the customer doesn't have enough loyalty points. + +*** + +## Test Out Cart Completion with Loyalty Points + +Since you now have the entire loyalty points flow implemented, you can test it out by going through the checkout flow, applying the loyalty points to the cart. + +When you place the order, if the customer has sufficient loyalty points, the validation hook will pass. + +Then, the `order.placed` event will be emitted, which will execute the subscriber that calls the `handleOrderPointsWorkflow`. + +In the workflow, since the order's cart has a loyalty promotion, the points equivalent to the promotion will be deducted, and the promotion becomes inactive. + +You can confirm that the loyalty points were deducted either by sending a request to the [retrieve loyalty points API route](#step-5-retrieve-loyalty-points-api-route), or by going through the checkout process again in the storefront. *** ## Next Steps -You have now implemented the logic to send abandoned cart notifications in Medusa. You can implement other customizations with Medusa, such as: +You've now implement a loyalty points system in Medusa. There's still more that you can implement based on your use case: -- [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). +- Add loyalty points on registration or other events. Refer to the [Events Reference](https://docs.medusajs.com/references/events/index.html.md) for a full list of available events you can listen to. +- Show the customer their loyalty point usage history. This will require adding another data model in the Loyalty Module that records the usage history. You can create records of that data model when an order that has a loyalty promotion is placed, then customize the storefront to show a new page for loyalty points history. +- Customize the Medusa Admin to show a new page or [UI Route](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md) for loyalty points information and analytics. -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. +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). @@ -48223,6 +49448,1141 @@ 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 + +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). + + # Use Saved Payment Methods During Checkout In this tutorial, you'll learn how to allow customers to save their payment methods and use them for future purchases. @@ -49067,2301 +51427,6 @@ If you're new to Medusa, check out the [main documentation](https://docs.medusaj To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md). -# 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. - -Medusa Cloud provides a beta Store Credits feature that facilitates building a loyalty point system. [Get in touch](https://medusajs.com/contact) for early access. - -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 management capabilities related to carts, orders, promotions, and more. - -A loyalty point system allows customers to earn points for purchases, which can be redeemed for discounts or rewards. In this tutorial, you'll learn how to customize the Medusa application to implement a loyalty points system. - -You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer. - -## Summary - -By following this tutorial, you will learn how to: - -- Install and set up Medusa. -- Define models to store loyalty points and the logic to manage them. -- Build flows that allow customers to earn and redeem points during checkout. - - Points are redeemed through dynamic promotions specific to the customer. -- Customize the cart completion flow to validate applied loyalty points. - -![Diagram illustrating redeem loyalty points flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1744126213/Medusa%20Resources/redeem-points-flow_kzgkux.jpg) - -- [Loyalty Points Repository](https://github.com/medusajs/examples/tree/main/loyalty-points): Find the full code for this guide in this repository. -- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1744212595/OpenApi/Loyalty-Points_jwi5e9.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. - -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. 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: Create Loyalty Module - -In Medusa, you can build custom features in a [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). 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 the module, you define the data models necessary for a feature and the logic to manage these data models. Later, you can build commerce flows around your module. - -In this step, you'll build a Loyalty Module that defines the necessary data models to store and manage loyalty points for customers. - -Refer to the [Modules documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) to learn more. - -### Create Module Directory - -Modules are created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/loyalty`. - -### Create Data Models - -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. - -Refer to the [Data Models documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#1-create-data-model/index.html.md) to learn more. - -For the Loyalty Module, you need to define a `LoyaltyPoint` data model that represents a customer's loyalty points. So, create the file `src/modules/loyalty/models/loyalty-point.ts` with the following content: - -```ts title="src/modules/loyalty/models/loyalty-point.ts" highlights={dmlHighlights} -import { model } from "@medusajs/framework/utils" - -const LoyaltyPoint = model.define("loyalty_point", { - id: model.id().primaryKey(), - points: model.number().default(0), - customer_id: model.text().unique("IDX_LOYALTY_CUSTOMER_ID"), -}) - -export default LoyaltyPoint -``` - -You define the `LoyaltyPoint` data model using the `model.define` method of the DML. It accepts the data model's table name as a first parameter, and the model's schema object as a second parameter. - -The `LoyaltyPoint` data model has the following properties: - -- `id`: A unique ID for the loyalty points. -- `points`: The number of loyalty points a customer has. -- `customer_id`: The ID of the customer who owns the loyalty points. This property has a unique index to ensure that each customer has only one record in the `loyalty_point` table. - -Learn more about defining data model properties in the [Property Types documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties/index.html.md). - -### Create Module's Service - -You now have the necessary data model in the Loyalty Module, but you'll need to manage its records. You do this by creating a service in the module. - -A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to the database, allowing you to manage your data models, or connect to a third-party service, which is useful if you're integrating with external services. - -Refer to the [Module Service documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#2-create-service/index.html.md) to learn more. - -To create the Loyalty Module's service, create the file `src/modules/loyalty/service.ts` with the following content: - -```ts title="src/modules/loyalty/service.ts" -import { MedusaError, MedusaService } from "@medusajs/framework/utils" -import LoyaltyPoint from "./models/loyalty-point" -import { InferTypeOf } from "@medusajs/framework/types" - -type LoyaltyPoint = InferTypeOf - -class LoyaltyModuleService extends MedusaService({ - LoyaltyPoint, -}) { - // TODO add methods -} - -export default LoyaltyModuleService -``` - -The `LoyaltyModuleService` extends `MedusaService` from the Modules SDK which generates a class with data-management methods for your module's data models. This saves you time on implementing Create, Read, Update, and Delete (CRUD) methods. - -So, the `LoyaltyModuleService` class now has methods like `createLoyaltyPoints` and `retrieveLoyaltyPoint`. - -Find all methods generated by the `MedusaService` in [the Service Factory reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/index.html.md). - -#### Add Methods to the Service - -Aside from the basic CRUD methods, you need to add methods that handle custom functionalities related to loyalty points. - -First, you need a method that adds loyalty points for a customer. Add the following method to the `LoyaltyModuleService`: - -```ts title="src/modules/loyalty/service.ts" -class LoyaltyModuleService extends MedusaService({ - LoyaltyPoint, -}) { - async addPoints(customerId: string, points: number): Promise { - const existingPoints = await this.listLoyaltyPoints({ - customer_id: customerId, - }) - - if (existingPoints.length > 0) { - return await this.updateLoyaltyPoints({ - id: existingPoints[0].id, - points: existingPoints[0].points + points, - }) - } - - return await this.createLoyaltyPoints({ - customer_id: customerId, - points, - }) - } -} -``` - -You add an `addPoints` method that accepts two parameters: the ID of the customer and the points to add. - -In the method, you retrieve the customer's existing loyalty points using the `listLoyaltyPoints` method, which is automatically generated by the `MedusaService`. If the customer has existing points, you update them with the new points using the `updateLoyaltyPoints` method. - -Otherwise, if the customer doesn't have existing loyalty points, you create a new record with the `createLoyaltyPoints` method. - -The next method you'll add deducts points from the customer's loyalty points, which is useful when the customer redeems points. Add the following method to the `LoyaltyModuleService`: - -```ts title="src/modules/loyalty/service.ts" -class LoyaltyModuleService extends MedusaService({ - LoyaltyPoint, -}) { - // ... - async deductPoints(customerId: string, points: number): Promise { - const existingPoints = await this.listLoyaltyPoints({ - customer_id: customerId, - }) - - if (existingPoints.length === 0 || existingPoints[0].points < points) { - throw new MedusaError( - MedusaError.Types.NOT_ALLOWED, - "Insufficient loyalty points" - ) - } - - return await this.updateLoyaltyPoints({ - id: existingPoints[0].id, - points: existingPoints[0].points - points, - }) - } -} -``` - -The `deductPoints` method accepts the customer ID and the points to deduct. - -In the method, you retrieve the customer's existing loyalty points using the `listLoyaltyPoints` method. If the customer doesn't have existing points or if the points to deduct are greater than the existing points, you throw an error. - -Otherwise, you update the customer's loyalty points with the new value using the `updateLoyaltyPoints` method, which is automatically generated by `MedusaService`. - -Next, you'll add the method that retrieves the points of a customer. Add the following method to the `LoyaltyModuleService`: - -```ts title="src/modules/loyalty/service.ts" -class LoyaltyModuleService extends MedusaService({ - LoyaltyPoint, -}) { - // ... - async getPoints(customerId: string): Promise { - const points = await this.listLoyaltyPoints({ - customer_id: customerId, - }) - - return points[0]?.points || 0 - } -} -``` - -The `getPoints` method accepts the customer ID and retrieves the customer's loyalty points using the `listLoyaltyPoints` method. If the customer has no points, it returns `0`. - -#### Add Method to Map Points to Discount - -Finally, you'll add a method that implements the logic of mapping loyalty points to a discount amount. This is useful when the customer wants to redeem their points during checkout. - -The mapping logic may differ for each use case. For example, you may need to use a third-party service to map the loyalty points discount amount, or use some custom calculation. - -To simplify the logic in this tutorial, you'll use a simple calculation that maps 1 point to 1 currency unit. For example, `100` points = `$100` discount. - -Add the following method to the `LoyaltyModuleService`: - -```ts title="src/modules/loyalty/service.ts" -class LoyaltyModuleService extends MedusaService({ - LoyaltyPoint, -}) { - // ... - async calculatePointsFromAmount(amount: number): Promise { - // Convert amount to points using a standard conversion rate - // For example, $1 = 1 point - // Round down to nearest whole point - const points = Math.floor(amount) - - if (points < 0) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Amount cannot be negative" - ) - } - - return points - } -} -``` - -The `calculatePointsFromAmount` method accepts the amount and converts it to the nearest whole number of points. If the amount is negative, it throws an error. - -You'll use this method later to calculate the amount discounted when a customer redeems their loyalty points. - -### 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/loyalty/index.ts` with the following content: - -```ts title="src/modules/loyalty/index.ts" -import { Module } from "@medusajs/framework/utils" -import LoyaltyModuleService from "./service" - -export const LOYALTY_MODULE = "loyalty" - -export default Module(LOYALTY_MODULE, { - service: LoyaltyModuleService, -}) -``` - -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 `loyalty`. -2. An object with a required property `service` indicating the module's service. - -You also export the module's name as `LOYALTY_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/loyalty", - }, - ], -}) -``` - -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. - -### Generate Migrations - -Since data models represent tables in the database, you define how they're created in the database with migrations. A migration is a TypeScript or JavaScript file that defines database changes made by a module. - -Refer to the [Migrations documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#5-generate-migrations/index.html.md) to learn more. - -Medusa's CLI tool can generate the migrations for you. To generate a migration for the Loyalty Module, run the following command in your Medusa application's directory: - -```bash -npx medusa db:generate loyalty -``` - -The `db:generate` command of the Medusa CLI accepts the name of the module to generate the migration for. You'll now have a `migrations` directory under `src/modules/loyalty` that holds the generated migration. - -Then, to reflect these migrations on the database, run the following command: - -```bash -npx medusa db:migrate -``` - -The table for the `LoyaltyPoint` data model is now created in the database. - -*** - -## Step 3: Change Loyalty Points Flow - -Now that you have a module that stores and manages loyalty points in the database, you'll start building flows around it that allow customers to earn and redeem points. - -The first flow you'll build will either add points to a customer's loyalty points or deduct them based on a purchased order. If the customer hasn't redeemed points, the points are added to their loyalty points. Otherwise, the points are deducted from their loyalty points. - -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's a special function that allows you to track its executions' progress, define roll-back logic, and configure other advanced features. Then, you execute the workflow from other customizations, such as in an endpoint. - -In this section, you'll build the workflow that adds or deducts loyalty points for an order's customer. Later, you'll execute this workflow when an order is placed. - -Learn more about workflows in the [Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). - -The workflow will have the following steps: - -- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the order's details. -- [validateCustomerExistsStep](#validateCustomerExistsStep): Validate that the customer is registered. -- [getCartLoyaltyPromoStep](#getCartLoyaltyPromoStep): Retrieve the cart's loyalty promotion. - -Medusa provides the `useQueryGraphStep` and `updatePromotionsStep` in its `@medusajs/medusa/core-flows` package. So, you'll only implement the other steps. - -### validateCustomerExistsStep - -In the workflow, you first need to validate that the customer is registered. Only registered customers can earn and redeem loyalty points. - -To do this, create the file `src/workflows/steps/validate-customer-exists.ts` with the following content: - -```ts title="src/workflows/steps/validate-customer-exists.ts" -import { CustomerDTO } from "@medusajs/framework/types" -import { createStep } from "@medusajs/framework/workflows-sdk" -import { MedusaError } from "@medusajs/framework/utils" - -export type ValidateCustomerExistsStepInput = { - customer: CustomerDTO | null | undefined -} - -export const validateCustomerExistsStep = createStep( - "validate-customer-exists", - async ({ customer }: ValidateCustomerExistsStepInput) => { - if (!customer) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Customer not found" - ) - } - - if (!customer.has_account) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Customer must have an account to earn or manage points" - ) - } - } -) -``` - -You create a step with `createStep` from the Workflows SDK. It accepts two parameters: - -1. The step's unique name, which is `validate-customer-exists`. -2. An async function that receives two parameters: - - The step's input, which is in this case an object with the customer's details. - - 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 validate that the customer is defined and that it's registered based on its `has_account` property. Otherwise, you throw an error. - -### getCartLoyaltyPromoStep - -Next, you'll need to retrieve the loyalty promotion applied on the cart, if there's any. This is useful to determine whether the customer has redeemed points. - -Before you create a step, you'll create a utility function that the step uses to retrieve the loyalty promotion of a cart. You'll create it as a separate utility function to use it later in other customizations. - -Create the file `src/utils/promo.ts` with the following content: - -```ts title="src/utils/promo.ts" -import { PromotionDTO, CustomerDTO, CartDTO } from "@medusajs/framework/types" - -export type CartData = CartDTO & { - promotions?: PromotionDTO[] - customer?: CustomerDTO - metadata: { - loyalty_promo_id?: string - } -} - -export function getCartLoyaltyPromotion( - cart: CartData -): PromotionDTO | undefined { - if (!cart?.metadata?.loyalty_promo_id) { - return - } - - return cart.promotions?.find( - (promotion) => promotion.id === cart.metadata.loyalty_promo_id - ) -} -``` - -You create a `getCartLoyaltyPromotion` function that accepts the cart's details as an input and returns the loyalty promotion if it exists. You retrieve the loyalty promotion if its ID is stored in the cart's `metadata.loyalty_promo_id` property. - -You can now create the step that uses this utility to retrieve a carts loyalty points promotion. To create the step, create the file `src/workflows/steps/get-cart-loyalty-promo.ts` with the following content: - -```ts title="src/workflows/steps/get-cart-loyalty-promo.ts" -import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" -import { CartData, getCartLoyaltyPromotion } from "../../utils/promo" -import { MedusaError } from "@medusajs/framework/utils" - -type GetCartLoyaltyPromoStepInput = { - cart: CartData, - throwErrorOn?: "found" | "not-found" -} - -export const getCartLoyaltyPromoStep = createStep( - "get-cart-loyalty-promo", - async ({ cart, throwErrorOn }: GetCartLoyaltyPromoStepInput) => { - const loyaltyPromo = getCartLoyaltyPromotion(cart) - - if (throwErrorOn === "found" && loyaltyPromo) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Loyalty promotion already applied to cart" - ) - } else if (throwErrorOn === "not-found" && !loyaltyPromo) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "No loyalty promotion found on cart" - ) - } - - return new StepResponse(loyaltyPromo) - } -) -``` - -You create a step that accepts an object having the following properties: - -- `cart`: The cart's details. -- `throwErrorOn`: An optional property that indicates whether to throw an error if the loyalty promotion is found or not found. - -The `throwErrorOn` property is useful to make the step reusable in different scenarios, allowing you to use it in later workflows. - -In the step, you call the `getCartLoyaltyPromotion` utility to retrieve the loyalty promotion. If the `throwErrorOn` property is set to `found` and the loyalty promotion is found, you throw an error. - -Otherwise, if the `throwErrorOn` property is set to `not-found` and the loyalty promotion is not found, you throw an error. - -To return data from a step, you return an instance of `StepResponse` from the Workflows SDK. It accepts as a parameter the data to return, which is the loyalty promotion in this case. - -### deductPurchasePointsStep - -If the order's cart has a loyalty promotion, you need to deduct points from the customer's loyalty points. To do this, create the file `src/workflows/steps/deduct-purchase-points.ts` with the following content: - -```ts title="src/workflows/steps/deduct-purchase-points.ts" highlights={deductStepHighlights} collapsibleLines="1-7" expandButtonLabel="Show Imports" -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { LOYALTY_MODULE } from "../../modules/loyalty" -import LoyaltyModuleService from "../../modules/loyalty/service" - -type DeductPurchasePointsInput = { - customer_id: string - amount: number -} - -export const deductPurchasePointsStep = createStep( - "deduct-purchase-points", - async ({ - customer_id, amount, - }: DeductPurchasePointsInput, { container }) => { - const loyaltyModuleService: LoyaltyModuleService = container.resolve( - LOYALTY_MODULE - ) - - const pointsToDeduct = await loyaltyModuleService.calculatePointsFromAmount( - amount - ) - - const result = await loyaltyModuleService.deductPoints( - customer_id, - pointsToDeduct - ) - - return new StepResponse(result, { - customer_id, - points: pointsToDeduct, - }) - }, - async (data, { container }) => { - if (!data) { - return - } - - const loyaltyModuleService: LoyaltyModuleService = container.resolve( - LOYALTY_MODULE - ) - - // Restore points in case of failure - await loyaltyModuleService.addPoints( - data.customer_id, - data.points - ) - } -) -``` - -You create a step that accepts an object having the following properties: - -- `customer_id`: The ID of the customer to deduct points from. -- `amount`: The promotion's amount, which will be used to calculate the points to deduct. - -In the step, you resolve the Loyalty Module's service from the Medusa container. Then, you use the `calculatePointsFromAmount` method to calculate the points to deduct from the promotion's amount. - -After that, you call the `deductPoints` method to deduct the points from the customer's loyalty points. - -Finally, you return a `StepResponse` with the result of the `deductPoints`. - -#### Compensation Function - -This step has a compensation function, which is passed as a third parameter to the `createStep` 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. - -The compensation function accepts two parameters: - -1. Data passed from the step function to the compensation function. The data is passed as a second parameter of the returned `StepResponse` instance. -2. An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). - -In the compensation function, you resolve the Loyalty Module's service from the Medusa container. Then, you call the `addPoints` method to restore the points deducted from the customer's loyalty points if an error occurs. - -### addPurchaseAsPointsStep - -The last step you'll create adds points to the customer's loyalty points. You'll use this step if the customer didn't redeem points during checkout. - -To create the step, create the file `src/workflows/steps/add-purchase-as-points.ts` with the following content: - -```ts title="src/workflows/steps/add-purchase-as-points.ts" highlights={addPointsHighlights} collapsibleLines="1-7" expandButtonLabel="Show Imports" -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { LOYALTY_MODULE } from "../../modules/loyalty" -import LoyaltyModuleService from "../../modules/loyalty/service" - -type StepInput = { - customer_id: string - amount: number -} - -export const addPurchaseAsPointsStep = createStep( - "add-purchase-as-points", - async (input: StepInput, { container }) => { - const loyaltyModuleService: LoyaltyModuleService = container.resolve( - LOYALTY_MODULE - ) - - const pointsToAdd = await loyaltyModuleService.calculatePointsFromAmount( - input.amount - ) - - const result = await loyaltyModuleService.addPoints( - input.customer_id, - pointsToAdd - ) - - return new StepResponse(result, { - customer_id: input.customer_id, - points: pointsToAdd, - }) - }, - async (data, { container }) => { - if (!data) { - return - } - - const loyaltyModuleService: LoyaltyModuleService = container.resolve( - LOYALTY_MODULE - ) - - await loyaltyModuleService.deductPoints( - data.customer_id, - data.points - ) - } -) -``` - -You create a step that accepts an object having the following properties: - -- `customer_id`: The ID of the customer to add points to. -- `amount`: The order's amount, which will be used to calculate the points to add. - -In the step, you resolve the Loyalty Module's service from the Medusa container. Then, you use the `calculatePointsFromAmount` method to calculate the points to add from the order's amount. - -After that, you call the `addPoints` method to add the points to the customer's loyalty points. - -Finally, you return a `StepResponse` with the result of the `addPoints`. - -You also pass to the compensation function the customer's ID and the points added. In the compensation function, you deduct the points if an error occurs. - -### Add Utility Functions - -Before you create the workflow, you need a utility function that checks whether an order's cart has a loyalty promotion. This is useful to determine whether the customer redeemed points during checkout, allowing you to decide which steps to execute. - -To add the utility function, add the following to `src/utils/promo.ts`: - -```ts title="src/utils/promo.ts" -import { OrderDTO } from "@medusajs/framework/types" - -export type OrderData = OrderDTO & { - promotion?: PromotionDTO[] - customer?: CustomerDTO - cart?: CartData -} - -export const CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE = "customer_id" - -export function orderHasLoyaltyPromotion(order: OrderData): boolean { - const loyaltyPromotion = getCartLoyaltyPromotion( - order.cart as unknown as CartData - ) - - return loyaltyPromotion?.rules?.some((rule) => { - return rule?.attribute === CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE && ( - rule?.values?.some((value) => value.value === order.customer?.id) || false - ) - }) || false -} -``` - -You first define an `OrderData` type that extends the `OrderDTO` type. This type has the order's details, including the cart, customer, and promotions details. - -Then, you define a constant `CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE` that represents the attribute used in the promotion rule to check whether the customer ID is valid. - -Finally, you create the `orderHasLoyaltyPromotion` function that accepts an order's details and checks whether it has a loyalty promotion. It returns `true` if: - -- The order's cart has a loyalty promotion. You use the `getCartLoyaltyPromotion` utility to try to retrieve the loyalty promotion. -- The promotion's rules include the `customer_id` attribute and its value matches the order's customer ID. - - When you create the promotion for the cart later, you'll see how to set this rule. - -You'll use this utility in the workflow next. - -### Create the Workflow - -Now that you have all the steps, you can create the workflow that uses them. - -To create the workflow, create the file `src/workflows/handle-order-points.ts` with the following content: - -```ts title="src/workflows/handle-order-points.ts" highlights={handleOrderPointsHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports" -import { createWorkflow, when } from "@medusajs/framework/workflows-sdk" -import { updatePromotionsStep, useQueryGraphStep } from "@medusajs/medusa/core-flows" -import { validateCustomerExistsStep, ValidateCustomerExistsStepInput } from "./steps/validate-customer-exists" -import { deductPurchasePointsStep } from "./steps/deduct-purchase-points" -import { addPurchaseAsPointsStep } from "./steps/add-purchase-as-points" -import { OrderData, CartData } from "../utils/promo" -import { orderHasLoyaltyPromotion } from "../utils/promo" -import { getCartLoyaltyPromoStep } from "./steps/get-cart-loyalty-promo" - -type WorkflowInput = { - order_id: string -} - -export const handleOrderPointsWorkflow = createWorkflow( - "handle-order-points", - ({ order_id }: WorkflowInput) => { - // @ts-ignore - const { data: orders } = useQueryGraphStep({ - entity: "order", - fields: [ - "id", - "customer.*", - "total", - "cart.*", - "cart.promotions.*", - "cart.promotions.rules.*", - "cart.promotions.rules.values.*", - "cart.promotions.application_method.*", - ], - filters: { - id: order_id, - }, - options: { - throwIfKeyNotFound: true, - }, - }) - - validateCustomerExistsStep({ - customer: orders[0].customer, - } as ValidateCustomerExistsStepInput) - - const loyaltyPointsPromotion = getCartLoyaltyPromoStep({ - cart: orders[0].cart as unknown as CartData, - }) - - when(orders, (orders) => - orderHasLoyaltyPromotion(orders[0] as unknown as OrderData) && - loyaltyPointsPromotion !== undefined - ) - .then(() => { - deductPurchasePointsStep({ - customer_id: orders[0].customer!.id, - amount: loyaltyPointsPromotion.application_method!.value as number, - }) - - updatePromotionsStep([ - { - id: loyaltyPointsPromotion.id, - status: "inactive", - }, - ]) - }) - - - when( - orders, - (order) => !orderHasLoyaltyPromotion(order[0] as unknown as OrderData) - ) - .then(() => { - addPurchaseAsPointsStep({ - customer_id: orders[0].customer!.id, - amount: orders[0].total, - }) - }) - } -) -``` - -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 with the order's ID. - -In the workflow's constructor function, you: - -- Use `useQueryGraphStep` to retrieve the order's details. You pass the order's ID as a filter to retrieve the order. - - This step uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), which is a tool that retrieves data across modules. -- Validate that the customer is registered using the `validateCustomerExistsStep`. -- Retrieve the cart's loyalty promotion using the `getCartLoyaltyPromoStep`. -- Use `when` to check whether the order's cart has a loyalty promotion. - - Since you can't perform data manipulation in a workflow's constructor function, `when` allows you to perform steps if a condition is satisfied. - - You pass as a first parameter the object to perform the condition on, which is the order in this case. In the second parameter, you pass a function that returns a boolean value, indicating whether the condition is satisfied. - - To specify the steps to perform if a condition is satisfied, you chain a `then` method to the `when` method. You can perform any step within the `then` method. - - In this case, if the order's cart has a loyalty promotion, you call the `deductPurchasePointsStep` to deduct points from the customer's loyalty points. You also call the `updatePromotionsStep` to deactivate the cart's loyalty promotion. -- You use another `when` to check whether the order's cart doesn't have a loyalty promotion. - - If the condition is satisfied, you call the `addPurchaseAsPointsStep` to add points to the customer's loyalty points. - -You'll use this workflow next when an order is placed. - -To learn more about the constraints on a workflow's constructor function, refer to the [Workflow Constraints](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md) documentation. Refer to the [When-Then](https://docs.medusajs.com/docs/learn/fundamentals/workflows/conditions/index.html.md) documentation to learn more about the `when` method and how to use it in a workflow. - -*** - -## Step 4: Handle Order Placed Event - -Now that you have the workflow that handles adding or deducting loyalty points for an order, you need to execute it when an order is placed. - -Medusa has an event system that allows you to listen to events emitted by the Medusa server using a [subscriber](https://docs.medusajs.com/docs//learn/fundamentals/events-and-subscribers/index.html.md). A subscriber is an asynchronous function that's executed when its associated event is emitted. In a subscriber, you can execute a workflow that performs actions in result of the event. - -In this step, you'll create a subscriber that listens to the `order.placed` event and executes the `handleOrderPointsWorkflow` workflow. - -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 fle `src/subscribers/order-placed.ts` with the following content: - -```ts title="src/subscribers/order-placed.ts" -import type { - SubscriberArgs, - SubscriberConfig, -} from "@medusajs/framework" -import { handleOrderPointsWorkflow } from "../workflows/handle-order-points" - -export default async function orderPlacedHandler({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - await handleOrderPointsWorkflow(container).run({ - input: { - order_id: data.id, - }, - }) -} - -export const config: SubscriberConfig = { - event: "order.placed", -} -``` - -The subscriber file must export: - -- An asynchronous subscriber function that's executed whenever the associated event, which is `order.placed` is triggered. -- A configuration object with an event property whose value is the event the subscriber is listening to. You can also pass an array of event names to listen to multiple events in the same subscriber. - -The subscriber function accepts an object with the following properties: - -- `event`: An object with the event's data payload. For example, the `order.placed` event has the order's ID in its data payload. -- `container`: The Medusa container, which you can use to resolve services and tools. - -In the subscriber function, you execute the `handleOrderPointsWorkflow` by invoking it, passing it the Medusa container, then using its `run` method, passing it the workflow's input. - -Whenever an order is placed now, the subscriber will be executed, which in turn will execute the workflow that handles the loyalty points flow. - -### Test it Out - -To test out the loyalty points flow, you'll use the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) that you installed in the first step. As mentioned in that step, the storefront will be installed in a separate directory from the Medusa application, and its name is `{project-name}-storefront`, where `{project-name}` is the name of your Medusa application's directory. - -So, run the following command in the Medusa application's directory to start the Medusa server: - -```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green" -npm run dev -``` - -Then, run the following command in the Next.js Starter Storefront's directory to start the Next.js server: - -```bash npm2yarn badgeLabel="Storefront" badgeColor="blue" -npm run dev -``` - -The Next.js Starter Storefront will be running on `http://localhost:8000`, and the Medusa server will be running on `http://localhost:9000`. - -Open the Next.js Starter Storefront in your browser and create a new account by going to Account at the top right. - -Once you're logged in, add an item to the cart and go through the checkout flow. - -After you place the order, you'll see the following message in your Medusa application's terminal: - -```bash -info: Processing order.placed which has 1 subscribers -``` - -This message indicates that the `order.placed` event was emitted, and that your subscriber was executed. - -Since you didn't redeem any points during checkout, loyalty points will be added to your account. You'll implement an API route that allows you to retrieve the loyalty points in the next step. - -*** - -## Step 5: Retrieve Loyalty Points API Route - -Next, you want to allow customers to view their loyalty points. You can show them on their profile page, or during checkout. - -To expose a feature to clients, you create 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/loyalty-points` that returns the loyalty points of the authenticated customer. - -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 `/store/customers/me/loyalty-points`, create the file `src/api/store/customers/me/loyalty-points/route.ts` with the following content: - -```ts title="src/api/store/customers/me/loyalty-points/route.ts" - -import { - AuthenticatedMedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { LOYALTY_MODULE } from "../../../../../modules/loyalty" -import LoyaltyModuleService from "../../../../../modules/loyalty/service" - -export async function GET( - req: AuthenticatedMedusaRequest, - res: MedusaResponse -) { - const loyaltyModuleService: LoyaltyModuleService = req.scope.resolve( - LOYALTY_MODULE - ) - - const points = await loyaltyModuleService.getPoints( - req.auth_context.actor_id - ) - - res.json({ - points, - }) -} -``` - -Since you export a `GET` route handler function, you're exposing a `GET` endpoint at `/store/customers/me/loyalty-points`. The route handler function accepts two parameters: - -1. A request object with details and context on the request, such as body parameters or authenticated customer details. -2. A response object to manipulate and send the response. - -In the route handler, you resolve the Loyalty Module's service from the Medusa container (which is available at `req.scope`). - -Then, you call the service's `getPoints` method to retrieve the authenticated customer's loyalty points. Note that routes starting with `/store/customers/me` are only accessible by authenticated customers. You can access the authenticated customer ID from the request's context, which is available at `req.auth_context.actor_id`. - -Finally, you return the loyalty points in the response. - -You'll test out this route as you customize the Next.js Starter Storefront next. - -*** - -## Step 6: Show Loyalty Points During Checkout - -Now that you have the API route to retrieve the loyalty points, you can show them during checkout. - -In this step, you'll customize the Next.js Starter Storefront to show the loyalty points in the checkout page. - -First, you'll add a server action function that retrieves the loyalty points from the route you created earlier. In `src/lib/data/customer.ts`, add the following function: - -```ts title="src/lib/data/customer.ts" badgeLabel="Storefront" badgeColor="blue" -export const getLoyaltyPoints = async () => { - const headers = { - ...(await getAuthHeaders()), - } - - return sdk.client.fetch<{ points: number }>( - `/store/customers/me/loyalty-points`, - { - method: "GET", - headers, - } - ) - .then(({ points }) => points) - .catch(() => null) -} -``` - -You add a `getLoyaltyPoints` function that retrieves the authenticated customer's loyalty points from the API route you created earlier. You pass the authentication headers using the `getAuthHeaders` function, which is a utility function defined in the Next.js Starter Storefront. - -If the customer isn't authenticated, the request will fail. So, you catch the error and return `null` in that case. - -Next, you'll create a component that shows the loyalty points in the checkout page. Create the file `src/modules/checkout/components/loyalty-points/index.tsx` with the following content: - -```tsx title="src/modules/checkout/components/loyalty-points/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={loyaltyPointsHighlights} -"use client" - -import { HttpTypes } from "@medusajs/types" -import { useEffect, useMemo, useState } from "react" -import { getLoyaltyPoints } from "../../../../lib/data/customer" -import { Button, Heading } from "@medusajs/ui" -import Link from "next/link" - -type LoyaltyPointsProps = { - cart: HttpTypes.StoreCart & { - promotions: HttpTypes.StorePromotion[] - } -} - -const LoyaltyPoints = ({ cart }: LoyaltyPointsProps) => { - const isLoyaltyPointsPromoApplied = useMemo(() => { - return cart.promotions.find( - (promo) => promo.id === cart.metadata?.loyalty_promo_id - ) !== undefined - }, [cart]) - const [loyaltyPoints, setLoyaltyPoints] = useState< - number | null - >(null) - - useEffect(() => { - getLoyaltyPoints() - .then((points) => { - console.log(points) - setLoyaltyPoints(points) - }) - }, []) - - const handleTogglePromotion = async ( - e: React.MouseEvent - ) => { - e.preventDefault() - // TODO apply or remove loyalty promotion - } - - return ( - <> -
-
- - Loyalty Points - - {loyaltyPoints === null && ( - - Sign up to get and use loyalty points - - )} - {loyaltyPoints !== null && ( -
- - - You have {loyaltyPoints} loyalty points - -
- )} -
- - ) -} - -export default LoyaltyPoints -``` - -You create a `LoyaltyPoints` component that accepts the cart's details as a prop. In the component, you: - -- Create a `isLoyaltyPointsPromoApplied` memoized value that checks whether the cart has a loyalty promotion applied. You use the `cart.metadata.loyalty_promo_id` property to check this. -- Create a `loyaltyPoints` state to store the customer's loyalty points. -- Call the `getLoyaltyPoints` function in a `useEffect` hook to retrieve the loyalty points from the API route you created earlier. You set the `loyaltyPoints` state with the retrieved points. -- Define `handleTogglePromotion` that, when clicked, would either apply or remove the promotion. You'll implement these functionalities later. -- Render the loyalty points in the component. If the customer isn't authenticated, you show a link to the account page to sign up. Otherwise, you show the loyalty points and a button to apply or remove the promotion. - -Next, you'll show this component at the end of the checkout's summary component. So, import the component in `src/modules/checkout/templates/checkout-summary/index.tsx`: - -```tsx title="src/modules/checkout/templates/checkout-summary/index.tsx" badgeLabel="Storefront" badgeColor="blue" -import LoyaltyPoints from "../../components/loyalty-points" -``` - -Then, in the return statement of the `CheckoutSummary` component, add the following after the `div` wrapping the `DiscountCode`: - -```tsx title="src/modules/checkout/templates/checkout-summary/index.tsx" badgeLabel="Storefront" badgeColor="blue" - -``` - -This will show the loyalty points component at the end of the checkout summary. - -### Test it Out - -To test out the customizations to the checkout flow, make sure both the Medusa application and Next.js Starter Storefront are running. - -Then, as an authenticated customer, add an item to cart and proceed to checkout. You'll find a new "Loyalty Points" section at the end of the checkout summary. - -![Loyalty Points Section at the end of the summary section at the right](https://res.cloudinary.com/dza7lstvk/image/upload/v1744195223/Medusa%20Resources/Screenshot_2025-04-09_at_1.39.34_PM_l5oltc.png) - -If you made a purchase before, you can see your loyalty points. You'll also see the "Apply Loyalty Points" button, which doesn't yet do anything. You'll add the functionality next. - -*** - -## Step 7: Apply Loyalty Points to Cart - -The next feature you'll implement allows the customer to apply their loyalty points during checkout. To implement the feature, you need: - -- A workflow that implements the steps of the apply loyalty points flow. -- An API route that exposes the workflow's functionality to clients. You'll then send a request to this API route to apply the loyalty points on the customer's cart. -- A function in the Next.js Starter Storefront that sends the request to the API route you created earlier. - -The workflow will have the following steps: - -- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details. -- [validateCustomerExistsStep](#validateCustomerExistsStep): Validate that the customer is registered. -- [getCartLoyaltyPromoStep](#getCartLoyaltyPromoStep): Retrieve the cart's loyalty promotion. -- [getCartLoyaltyPromoAmountStep](#getCartLoyaltyPromoAmountStep): Get the amount to be discounted based on the loyalty points. -- [createPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPromotionsStep/index.html.md): Create a new loyalty promotion for the cart. -- [updateCartPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/workflows/updateCartPromotionsWorkflow/index.html.md): Update the cart's promotions with the new loyalty promotion. -- [updateCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartsStep/index.html.md): Update the cart to store the ID of the loyalty promotion in the metadata. -- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details again. - -Most of the workflow's steps are either provided by Medusa in the `@medusajs/medusa/core-flows` package or steps you've already implemented. You only need to implement the `getCartLoyaltyPromoAmountStep` step. - -### getCartLoyaltyPromoAmountStep - -The fourth step in the workflow is the `getCartLoyaltyPromoAmountStep`, which retrieves the amount to be discounted based on the loyalty points. This step is useful to determine how much discount to apply to the cart. - -To create the step, create the file `src/workflows/steps/get-cart-loyalty-promo-amount.ts` with the following content: - -```ts title="src/workflows/steps/get-cart-loyalty-promo-amount.ts" highlights={getCartLoyaltyPromoAmountStepHighlights} -import { PromotionDTO, CustomerDTO } from "@medusajs/framework/types" -import { MedusaError } from "@medusajs/framework/utils" -import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" -import LoyaltyModuleService from "../../modules/loyalty/service" -import { LOYALTY_MODULE } from "../../modules/loyalty" - -export type GetCartLoyaltyPromoAmountStepInput = { - cart: { - id: string - customer: CustomerDTO - promotions?: PromotionDTO[] - total: number - } -} - -export const getCartLoyaltyPromoAmountStep = createStep( - "get-cart-loyalty-promo-amount", - async ({ cart }: GetCartLoyaltyPromoAmountStepInput, { container }) => { - // Check if customer has any loyalty points - const loyaltyModuleService: LoyaltyModuleService = container.resolve( - LOYALTY_MODULE - ) - const loyaltyPoints = await loyaltyModuleService.getPoints( - cart.customer.id - ) - - if (loyaltyPoints <= 0) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Customer has no loyalty points" - ) - } - - const pointsAmount = await loyaltyModuleService.calculatePointsFromAmount( - loyaltyPoints - ) - - const amount = Math.min(pointsAmount, cart.total) - - return new StepResponse(amount) - } -) -``` - -You create a step that accepts an object having the cart's details. - -In the step, you resolve the Loyalty Module's service from the Medusa container. Then, you call the `getPoints` method to retrieve the customer's loyalty points. If the customer has no loyalty points, you throw an error. - -Next, you call the `calculatePointsFromAmount` method to calculate the amount to be discounted based on the loyalty points. You use the `Math.min` function to ensure that the amount doesn't exceed the cart's total. - -Finally, you return a `StepResponse` with the amount to be discounted. - -### Create the Workflow - -You can now create the workflow that applies a loyalty promotion to the cart. - -To create the workflow, create the file `src/workflows/apply-loyalty-on-cart.ts` with the following content: - -```ts title="src/workflows/apply-loyalty-on-cart.ts" highlights={applyLoyaltyOnCartWorkflowHighlights} collapsibleLines="1-24" expandButtonLabel="Show Imports" -import { - createWorkflow, - transform, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" -import { - createPromotionsStep, - updateCartPromotionsWorkflow, - updateCartsStep, - useQueryGraphStep, -} from "@medusajs/medusa/core-flows" -import { - validateCustomerExistsStep, - ValidateCustomerExistsStepInput, -} from "./steps/validate-customer-exists" -import { - getCartLoyaltyPromoAmountStep, - GetCartLoyaltyPromoAmountStepInput, -} from "./steps/get-cart-loyalty-promo-amount" -import { CartData, CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE } from "../utils/promo" -import { CreatePromotionDTO } from "@medusajs/framework/types" -import { PromotionActions } from "@medusajs/framework/utils" -import { getCartLoyaltyPromoStep } from "./steps/get-cart-loyalty-promo" - -type WorkflowInput = { - cart_id: string -} - -const fields = [ - "id", - "customer.*", - "promotions.*", - "promotions.application_method.*", - "promotions.rules.*", - "promotions.rules.values.*", - "currency_code", - "total", - "metadata", -] - -export const applyLoyaltyOnCartWorkflow = createWorkflow( - "apply-loyalty-on-cart", - (input: WorkflowInput) => { - // @ts-ignore - const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields, - filters: { - id: input.cart_id, - }, - options: { - throwIfKeyNotFound: true, - }, - }) - - validateCustomerExistsStep({ - customer: carts[0].customer, - } as ValidateCustomerExistsStepInput) - - getCartLoyaltyPromoStep({ - cart: carts[0] as unknown as CartData, - throwErrorOn: "found", - }) - - const amount = getCartLoyaltyPromoAmountStep({ - cart: carts[0], - } as unknown as GetCartLoyaltyPromoAmountStepInput) - - // TODO create and apply the promotion on the cart - } -) -``` - -You create a workflow that accepts an object with the cart's ID as input. - -So far, you: - -- Use `useQueryGraphStep` to retrieve the cart's details. You pass the cart's ID as a filter to retrieve the cart. -- Validate that the customer is registered using the `validateCustomerExistsStep`. -- Check whether the cart has a loyalty promotion using the `getCartLoyaltyPromoStep`. You pass the `throwErrorOn` parameter with the value `found` to throw an error if a loyalty promotion is found in the cart. -- Retrieve the amount to be discounted based on the loyalty points using the `getCartLoyaltyPromoAmountStep`. - -Next, you need to create a new loyalty promotion for the cart. First, you'll prepare the data of the promotion to be created. - -Replace the `TODO` with the following: - -```ts title="src/workflows/apply-loyalty-on-cart.ts" highlights={prepareLoyaltyPromoDataHighlights} -const promoToCreate = transform({ - carts, - amount, -}, (data) => { - const randomStr = Math.random().toString(36).substring(2, 8) - const uniqueId = ( - "LOYALTY-" + data.carts[0].customer?.first_name + "-" + randomStr - ).toUpperCase() - return { - code: uniqueId, - type: "standard", - status: "active", - application_method: { - type: "fixed", - value: data.amount, - target_type: "order", - currency_code: data.carts[0].currency_code, - allocation: "across", - }, - rules: [ - { - attribute: CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE, - operator: "eq", - values: [data.carts[0].customer!.id], - }, - ], - campaign: { - name: uniqueId, - description: "Loyalty points promotion for " + data.carts[0].customer!.email, - campaign_identifier: uniqueId, - budget: { - type: "usage", - limit: 1, - }, - }, - } -}) - -// TODO create promotion and apply it on cart -``` - -Since data manipulation isn't allowed in a workflow constructor, you use the [transform](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) function from the Workflows SDK. It accepts two parameters: - -- The data to perform manipulation on. In this case, you pass the cart's details and the amount to be discounted. -- A function that receives the data from the first parameter, and returns the transformed data. - -In the transformation function, you prepare th data of the loyalty promotion to be created. Some key details include: - -- You set the discount amount in the application method of the promotion. -- You add a rule to the promotion that ensures it can be used only in carts having their `customer_id` equal to this customer's ID. This prevents other customers from using this promotion. -- You create a campaign for the promotion, and you set the campaign budget to a single usage. This prevents the customer from using the promotion again. - -Learn more about promotion concepts in the [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md)'s documentation. - -You can now use the returned data to create a promotion and apply it to the cart. Replace the new `TODO` with the following: - -```ts title="src/workflows/apply-loyalty-on-cart.ts" highlights={createLoyaltyPromoStepHighlights} -const loyaltyPromo = createPromotionsStep([ - promoToCreate, -] as CreatePromotionDTO[]) - -const { metadata, ...updatePromoData } = transform({ - carts, - promoToCreate, - loyaltyPromo, -}, (data) => { - const promos = [ - ...(data.carts[0].promotions?.map((promo) => promo?.code).filter(Boolean) || []) as string[], - data.promoToCreate.code, - ] - - return { - cart_id: data.carts[0].id, - promo_codes: promos, - action: PromotionActions.ADD, - metadata: { - loyalty_promo_id: data.loyaltyPromo[0].id, - }, - } -}) - -updateCartPromotionsWorkflow.runAsStep({ - input: updatePromoData, -}) - -updateCartsStep([ - { - id: input.cart_id, - metadata, - }, -]) - -// retrieve cart with updated promotions -// @ts-ignore -const { data: updatedCarts } = useQueryGraphStep({ - entity: "cart", - fields, - filters: { id: input.cart_id }, -}).config({ name: "retrieve-cart" }) - -return new WorkflowResponse(updatedCarts[0]) -``` - -In the rest of the workflow, you: - -- Create the loyalty promotion using the data you prepared earlier using the `createPromotionsStep`. -- Use the `transform` function to prepare the data to update the cart's promotions. You add the new loyalty promotion code to the cart's promotions codes, and set the `loyalty_promo_id` in the cart's metadata. -- Update the cart's promotions with the new loyalty promotion using the `updateCartPromotionsWorkflow` workflow. -- Update the cart's metadata with the loyalty promotion ID using the `updateCartsStep`. -- Retrieve the cart's details again using `useQueryGraphStep` to get the updated cart with the new loyalty promotion. - -To return data from the workflow, you must return an instance of `WorkflowResponse`. You pass it the data to be returned, which is in this case the cart's details. - -### Create the API Route - -Next, you'll create the API route that executes this workflow. - -To create the API route, create the file `src/api/store/carts/[id]/loyalty-points/route.ts` with the following content: - -```ts title="src/api/store/carts/[id]/loyalty-points/route.ts" -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" -import { applyLoyaltyOnCartWorkflow } from "../../../../../workflows/apply-loyalty-on-cart" - -export async function POST( - req: MedusaRequest, - res: MedusaResponse -) { - const { id: cart_id } = req.params - - const { result: cart } = await applyLoyaltyOnCartWorkflow(req.scope) - .run({ - input: { - cart_id, - }, - }) - - res.json({ cart }) -} -``` - -Since you export a `POST` route handler, you expose a `POST` API route at `/store/carts/[id]/loyalty-points`. - -In the route handler, you execute the `applyLoyaltyOnCartWorkflow` workflow, passing it the cart ID as an input. You return the cart's details in the response. - -You can now use this API route in the Next.js Starter Storefront. - -### Apply Loyalty Points in the Storefront - -In the Next.js Starter Storefront, you need to add a server action function that sends a request to the API route you created earlier. Then, you'll use that function when the customer clicks the "Apply Loyalty Points" button. - -To add the function, add the following to `src/lib/data/cart.ts` in the Next.js Starter Storefront: - -```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="blue" -export async function applyLoyaltyPointsOnCart() { - const cartId = await getCartId() - const headers = { - ...(await getAuthHeaders()), - } - - return await sdk.client.fetch<{ - cart: HttpTypes.StoreCart & { - promotions: HttpTypes.StorePromotion[] - } - }>(`/store/carts/${cartId}/loyalty-points`, { - method: "POST", - headers, - }) - .then(async (result) => { - const cartCacheTag = await getCacheTag("carts") - revalidateTag(cartCacheTag) - - return result - }) -} -``` - -You create an `applyLoyaltyPointsOnCart` function that sends a request to the API route you created earlier. - -In the function, you retrieve the cart ID stored in the cookie using the `getCartId` function, which is available in the Next.js Starter Storefront. - -Then, you send the request. Once the request is resolved successfully, you revalidate the cart cache tag to ensure that the cart's details are updated and refetched by other components. This ensures that the applied promotion is shown in the checkout summary without needing to refresh the page. - -Finally, you'll use this function in the `handleTogglePromotion` function in the `LoyaltyPoints` component you created earlier. - -At the top of `src/modules/checkout/components/loyalty-points/index.tsx`, import the function: - -```tsx title="src/modules/checkout/components/loyalty-points/index.tsx" badgeLabel="Storefront" badgeColor="blue" -import { applyLoyaltyPointsOnCart } from "../../../../lib/data/cart" -``` - -Then, replace the `handleTogglePromotion` function with the following: - -```tsx title="src/modules/checkout/components/loyalty-points/index.tsx" badgeLabel="Storefront" badgeColor="blue" -const handleTogglePromotion = async ( - e: React.MouseEvent -) => { - e.preventDefault() - if (!isLoyaltyPointsPromoApplied) { - await applyLoyaltyPointsOnCart() - } else { - // TODO remove loyalty points - } -} -``` - -In the `handleTogglePromotion` function, you call the `applyLoyaltyPointsOnCart` function if the cart doesn't have a loyalty promotion. This will send a request to the API route you created earlier, which will execute the workflow that applies the loyalty promotion to the cart. - -You'll implement removing the loyalty points promotion in a later step. - -### Test it Out - -To test out applying the loyalty points on the cart, start the Medusa application and Next.js Starter Storefront. - -Then, in the checkout flow as an authenticated customer, click on the "Apply Loyalty Points" button. The checkout summary will be updated with the applied promotion and the discount amount. - -If you don't want the promotion to be shown in the "Promotions(s) applied" section, you can filter the promotions in `src/modules/checkout/components/discount-code/index.tsx` to not show a promotion matching `cart.metadata.loyalty_promo_id`. - -![Discounted amount is shown as part of the summary and the promotion is shown as part of the applied promotions](https://res.cloudinary.com/dza7lstvk/image/upload/v1744200895/Medusa%20Resources/Screenshot_2025-04-09_at_3.14.19_PM_abmtjh.png) - -*** - -## Step 8: Remove Loyalty Points From Cart - -In this step, you'll implement the functionality to remove the loyalty points promotion from the cart. This is useful if the customer changes their mind and wants to remove the promotion. - -To implement this functionality, you'll need to: - -- Create a workflow that removes the loyalty points promotion from the cart. -- Create an API route that executes the workflow. -- Create a function in the Next.js Starter Storefront that sends a request to the API route you created earlier. -- Use the function in the `handleTogglePromotion` function in the `LoyaltyPoints` component you created earlier. - -### Create the Workflow - -The workflow will have the following steps: - -- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details. -- [getCartLoyaltyPromoStep](#getCartLoyaltyPromoStep): Retrieve the cart's loyalty promotion. -- [updateCartPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/workflows/updateCartPromotionsWorkflow/index.html.md): Update the cart's promotions to remove the loyalty promotion. -- [updateCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartsStep/index.html.md): Update the cart to remove the loyalty promotion ID from the metadata. -- [updatePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePromotionsStep/index.html.md): Deactive the loyalty promotion. -- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details again. - -Since you already have all the steps, you can create the workflow. - -To create the workflow, create the file `src/workflows/remove-loyalty-from-cart.ts` with the following content: - -```ts title="src/workflows/remove-loyalty-from-cart.ts" collapsibleLines="1-15" expandButtonLabel="Show Imports" highlights={removeLoyaltyFromCartWorkflowHighlights} -import { - createWorkflow, - transform, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" -import { - useQueryGraphStep, - updateCartPromotionsWorkflow, - updateCartsStep, - updatePromotionsStep, -} from "@medusajs/medusa/core-flows" -import { getCartLoyaltyPromoStep } from "./steps/get-cart-loyalty-promo" -import { PromotionActions } from "@medusajs/framework/utils" -import { CartData } from "../utils/promo" - -type WorkflowInput = { - cart_id: string -} - -const fields = [ - "id", - "customer.*", - "promotions.*", - "promotions.application_method.*", - "promotions.rules.*", - "promotions.rules.values.*", - "currency_code", - "total", - "metadata", -] - -export const removeLoyaltyFromCartWorkflow = createWorkflow( - "remove-loyalty-from-cart", - (input: WorkflowInput) => { - // @ts-ignore - const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields, - filters: { - id: input.cart_id, - }, - }) - - const loyaltyPromo = getCartLoyaltyPromoStep({ - cart: carts[0] as unknown as CartData, - throwErrorOn: "not-found", - }) - - updateCartPromotionsWorkflow.runAsStep({ - input: { - cart_id: input.cart_id, - promo_codes: [loyaltyPromo.code!], - action: PromotionActions.REMOVE, - }, - }) - - const newMetadata = transform({ - carts, - }, (data) => { - const { loyalty_promo_id, ...rest } = data.carts[0].metadata || {} - - return { - ...rest, - loyalty_promo_id: null, - } - }) - - updateCartsStep([ - { - id: input.cart_id, - metadata: newMetadata, - }, - ]) - - updatePromotionsStep([ - { - id: loyaltyPromo.id, - status: "inactive", - }, - ]) - - // retrieve cart with updated promotions - // @ts-ignore - const { data: updatedCarts } = useQueryGraphStep({ - entity: "cart", - fields, - filters: { id: input.cart_id }, - }).config({ name: "retrieve-cart" }) - - return new WorkflowResponse(updatedCarts[0]) - } -) -``` - -You create a workflow that accepts an object with the cart's ID as input. - -In the workflow, you: - -- Use `useQueryGraphStep` to retrieve the cart's details. You pass the cart's ID as a filter to retrieve the cart. -- Check whether the cart has a loyalty promotion using the `getCartLoyaltyPromoStep`. You pass the `throwErrorOn` parameter with the value `not-found` to throw an error if a loyalty promotion isn't found in the cart. -- Update the cart's promotions using the `updateCartPromotionsWorkflow`, removing the loyalty promotion. -- Use the `transform` function to prepare the new metadata of the cart. You remove the `loyalty_promo_id` from the metadata. -- Update the cart's metadata with the new metadata using the `updateCartsStep`. -- Deactivate the loyalty promotion using the `updatePromotionsStep`. -- Retrieve the cart's details again using `useQueryGraphStep` to get the updated cart with the new loyalty promotion. -- Return the cart's details in a `WorkflowResponse` instance. - -### Create the API Route - -Next, you'll create the API route that executes this workflow. - -To create the API route, add the following in `src/api/store/carts/[id]/loyalty-points/route.ts`: - -```ts title="src/api/store/carts/[id]/loyalty-points/route.ts" -// other imports... -import { removeLoyaltyFromCartWorkflow } from "../../../../../workflows/remove-loyalty-from-cart" - -// ... -export async function DELETE( - req: MedusaRequest, - res: MedusaResponse -) { - const { id: cart_id } = req.params - - const { result: cart } = await removeLoyaltyFromCartWorkflow(req.scope) - .run({ - input: { - cart_id, - }, - }) - - res.json({ cart }) -} -``` - -You export a `DELETE` route handler, which exposes a `DELETE` API route at `/store/carts/[id]/loyalty-points`. - -In the route handler, you execute the `removeLoyaltyFromCartWorkflow` workflow, passing it the cart ID as an input. You return the cart's details in the response. - -You can now use this API route in the Next.js Starter Storefront. - -### Remove Loyalty Points in the Storefront - -In the Next.js Starter Storefront, you need to add a server action function that sends a request to the API route you created earlier. Then, you'll use that function when the customer clicks the "Remove Loyalty Points" button, which shows when the cart has a loyalty promotion applied. - -To add the function, add the following to `src/lib/data/cart.ts`: - -```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="blue" -export async function removeLoyaltyPointsOnCart() { - const cartId = await getCartId() - const headers = { - ...(await getAuthHeaders()), - } - const next = { - ...(await getCacheOptions("carts")), - } - - return await sdk.client.fetch<{ - cart: HttpTypes.StoreCart & { - promotions: HttpTypes.StorePromotion[] - } - }>(`/store/carts/${cartId}/loyalty-points`, { - method: "DELETE", - headers, - }) - .then(async (result) => { - const cartCacheTag = await getCacheTag("carts") - revalidateTag(cartCacheTag) - - return result - }) -} -``` - -You create a `removeLoyaltyPointsOnCart` function that sends a request to the API route you created earlier. - -In the function, you retrieve the cart ID stored in the cookie using the `getCartId` function, which is available in the Next.js Starter Storefront. - -Then, you send the request to the API route. Once the request is resolved successfully, you revalidate the cart cache tag to ensure that the cart's details are updated and refetched by other components. This ensures that the promotion is removed from the checkout summary without needing to refresh the page. - -Finally, you'll use this function in the `handleTogglePromotion` function in the `LoyaltyPoints` component you created earlier. - -At the top of `src/modules/checkout/components/loyalty-points/index.tsx`, add the following import: - -```tsx title="src/modules/checkout/components/loyalty-points/index.tsx" badgeLabel="Storefront" badgeColor="blue" -import { removeLoyaltyPointsOnCart } from "../../../../lib/data/cart" -``` - -Then, replace the `TODO` in `handleTogglePromotion` with the following: - -```tsx title="src/modules/checkout/components/loyalty-points/index.tsx" badgeLabel="Storefront" badgeColor="blue" -await removeLoyaltyPointsOnCart() -``` - -In the `handleTogglePromotion` function, you call the `removeLoyaltyPointsOnCart` function if the cart has a loyalty promotion. This will send a request to the API route you created earlier, which will execute the workflow that removes the loyalty promotion from the cart. - -### Test it Out - -To test out removing the loyalty points from the cart, start the Medusa application and Next.js Starter Storefront. - -Then, in the checkout flow as an authenticated customer, after applying the loyalty points, click on the "Remove Loyalty Points" button. The checkout summary will be updated with the removed promotion and the discount amount. - -![The "Remove Loyalty Points" button is shown in the "Loyalty Points" section](https://res.cloudinary.com/dza7lstvk/image/upload/v1744204436/Medusa%20Resources/Screenshot_2025-04-09_at_4.13.24_PM_xt5trh.png) - -*** - -## Step 9: Validate Loyalty Points on Cart Completion - -After the customer applies the loyalty points to the cart and places the order, you need to validate that the customer actually has the loyalty points. This prevents edge cases where the customer may have applied the loyalty points previously but they don't have them anymore. - -So, in this step, you'll hook into Medusa's cart completion flow to perform the validation. - -Since Medusa uses workflows in its API routes, it allows you to hook into them and perform custom functionalities using [Workflow Hooks](https://docs.medusajs.com/docs/learn/fundamentals/workflows/workflow-hooks/index.html.md). A workflow hook is a point in a workflow where you can inject custom functionality as a step function, called a hook handler. - -Medusa uses the [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) hook to complete the cart and place an order. This workflow has a `validate` hook that allows you to perform custom validation before the cart is completed. - -To consume the `validate` hook, create the file `src/workflows/hooks/complete-cart.ts` with the following content: - -```ts title="src/workflows/hooks/complete-cart.ts" highlights={completeCartWorkflowHookHighlights} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import { completeCartWorkflow } from "@medusajs/medusa/core-flows" -import LoyaltyModuleService from "../../modules/loyalty/service" -import { LOYALTY_MODULE } from "../../modules/loyalty" -import { CartData, getCartLoyaltyPromotion } from "../../utils/promo" -import { MedusaError } from "@medusajs/framework/utils" - -completeCartWorkflow.hooks.validate( - async ({ cart }, { container }) => { - const query = container.resolve("query") - const loyaltyModuleService: LoyaltyModuleService = container.resolve( - LOYALTY_MODULE - ) - - const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "id", - "promotions.*", - "customer.*", - "promotions.rules.*", - "promotions.rules.values.*", - "promotions.application_method.*", - "metadata", - ], - filters: { - id: cart.id, - }, - }, { - throwIfKeyNotFound: true, - }) - - const loyaltyPromo = getCartLoyaltyPromotion( - carts[0] as unknown as CartData - ) - - if (!loyaltyPromo) { - return - } - - const customerLoyaltyPoints = await loyaltyModuleService.getPoints( - carts[0].customer!.id - ) - const requiredPoints = await loyaltyModuleService.calculatePointsFromAmount( - loyaltyPromo.application_method!.value as number - ) - - if (customerLoyaltyPoints < requiredPoints) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Customer does not have enough loyalty points. Required: ${ - requiredPoints - }, Available: ${customerLoyaltyPoints}` - ) - } - } -) -``` - -Workflows have a special `hooks` property that includes all the hooks tht you can consume in that workflow. You consume the hook by invoking it from the workflow's `hooks` property. - -Since the hook is essentially a step function, it accepts the following parameters: - -- The hook's input passed from the workflow, which differs for each hook. The `validate` hook receives an object having the cart's details. -- The step context object, which contains the Medusa container. You can use it to resolve services and perform actions. - -In the hook, you resolve Query and the Loyalty Module's service. Then, you use Query to retrieve the cart's necessary details, including its promotions, customer, and metadata. - -After that, you retrieve the customer's loyalty points and calculate the required points to apply the loyalty promotion. - -If the customer doesn't have enough loyalty points, you throw an error. This will prevent the cart from being completed if the customer doesn't have enough loyalty points. - -*** - -## Test Out Cart Completion with Loyalty Points - -Since you now have the entire loyalty points flow implemented, you can test it out by going through the checkout flow, applying the loyalty points to the cart. - -When you place the order, if the customer has sufficient loyalty points, the validation hook will pass. - -Then, the `order.placed` event will be emitted, which will execute the subscriber that calls the `handleOrderPointsWorkflow`. - -In the workflow, since the order's cart has a loyalty promotion, the points equivalent to the promotion will be deducted, and the promotion becomes inactive. - -You can confirm that the loyalty points were deducted either by sending a request to the [retrieve loyalty points API route](#step-5-retrieve-loyalty-points-api-route), or by going through the checkout process again in the storefront. - -*** - -## Next Steps - -You've now implement a loyalty points system in Medusa. There's still more that you can implement based on your use case: - -- Add loyalty points on registration or other events. Refer to the [Events Reference](https://docs.medusajs.com/references/events/index.html.md) for a full list of available events you can listen to. -- Show the customer their loyalty point usage history. This will require adding another data model in the Loyalty Module that records the usage history. You can create records of that data model when an order that has a loyalty promotion is placed, then customize the storefront to show a new page for loyalty points history. -- Customize the Medusa Admin to show a new page or [UI Route](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md) for loyalty points information and analytics. - -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). - - # Integrations You can integrate any third-party service to Medusa, including storage services, notification systems, Content-Management Systems (CMS), etc… By integrating third-party services, you build flows and synchronize data around these integrations, making Medusa not only your commerce application, but a middleware layer between your data sources and operations. @@ -52674,6 +52739,1054 @@ If you're new to Medusa, check out the [main documentation](https://docs.medusaj To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md). +# How to Build Magento Data Migration Plugin + +In this tutorial, you'll learn how to build a [plugin](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md) that migrates data, such as products, from Magento to Medusa. + +Magento is known for its customization capabilities. However, its monolithic architecture imposes limitations on business requirements, often forcing development teams to implement hacky workarounds. Over time, these customizations become challenging to maintain, especially as the business scales, leading to increased technical debt and slower feature delivery. + +Medusa's modular architecture allows you to build a custom digital commerce platform that meets your business requirements without the limitations of a monolithic system. By migrating from Magento to Medusa, you can take advantage of Medusa's modern technology stack to build a scalable and flexible commerce platform that grows with your business. + +By following this tutorial, you'll create a Medusa plugin that migrates data from a Magento server to a Medusa application in minimal time. You can re-use this plugin across multiple Medusa applications, allowing you to adopt Medusa across your projects. + +## Summary + +### Prerequisites + + + +This tutorial will teach you how to: + +- Install and set up a Medusa application project. +- Install and set up a Medusa plugin. +- Implement a Magento Module in the plugin to connect to Magento's APIs and retrieve products. + - This guide will only focus on migrating product data from Magento to Medusa. You can extend the implementation to migrate other data, such as customers, orders, and more. +- Trigger data migration from Magento to Medusa in a scheduled job. + +You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer. + +![Diagram showcasing the flow of migrating data from Magento to Medusa](https://res.cloudinary.com/dza7lstvk/image/upload/v1739360550/Medusa%20Resources/magento-summary_hsewci.jpg) + +[Example Repository](https://github.com/medusajs/examples/tree/main/migrate-from-magento): Find the full code of the guide in this repository. The repository also includes additional features, such as triggering migrations from the Medusa Admin dashboard. + +*** + +## Step 1: Install a Medusa Application + +You'll first install a Medusa application that exposes core commerce features through REST APIs. You'll later install the Magento plugin in this application to test it out. + +### Prerequisites + +- [Node.js v20+](https://nodejs.org/en/download) +- [Git CLI tool](https://git-scm.com/downloads) +- [PostgreSQL](https://www.postgresql.org/download/) + +Start by installing the Medusa application on your machine with the following command: + +```bash +npx create-medusa-app@latest +``` + +You'll be asked for the project's name. You can also optionally choose to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md). + +Afterward, the installation process will start, which will install the Medusa application in a directory with your project's name. If you chose to install the Next.js starter, it'll be installed in a separate directory with the `{project-name}-storefront` name. + +The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Refer to the [Medusa Architecture](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md) documentation to learn more. + +Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterward, you can log in with the new user and explore the dashboard. + +Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help. + +*** + +## Step 2: Install a Medusa Plugin Project + +A plugin is a package of reusable Medusa customizations that you can install in any Medusa application. You can add in the plugin [API Routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md), [Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), and other customizations, as you'll see in this guide. Afterward, you can test it out locally in a Medusa application, then publish it to npm to install and use it in any Medusa application. + +Refer to the [Plugins](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md) documentation to learn more about plugins. + +A Medusa plugin is set up in a different project, giving you the flexibility in building and publishing it, while providing you with the tools to test it out locally in a Medusa application. + +To create a new Medusa plugin project, run the following command in a directory different than that of the Medusa application: + +```bash npm2yarn +npx create-medusa-app@latest medusa-plugin-magento --plugin +``` + +Where `medusa-plugin-magento` is the name of the plugin's directory and the name set in the plugin's `package.json`. So, if you wish to publish it to NPM later under a different name, you can change it here in the command or later in `package.json`. + +Once the installation process is done, a new directory named `medusa-plugin-magento` will be created with the plugin project files. + +![Directory structure of a plugin project](https://res.cloudinary.com/dza7lstvk/image/upload/v1737019441/Medusa%20Book/project-dir_q4xtri.jpg) + +*** + +## Step 3: Set up Plugin in Medusa Application + +Before you start your development, you'll set up the plugin in the Medusa application you installed in the first step. This will allow you to test the plugin during your development process. + +In the plugin's directory, run the following command to publish the plugin to the local package registry: + +```bash title="Plugin project" +npx medusa plugin:publish +``` + +This command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. The plugin is published locally under the name you specified in `package.json`. + +Next, you'll install the plugin in the Medusa application from the local registry. + +If you've installed your Medusa project before v2.3.1, you must install [yalc](https://github.com/wclr/yalc) as a development dependency first. + +Run the following command in the Medusa application's directory to install the plugin: + +```bash title="Medusa application" +npx medusa plugin:add medusa-plugin-magento +``` + +This command installs the plugin in the Medusa application from the local package registry. + +Next, register the plugin in the `medusa-config.ts` file of the Medusa application: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + plugins: [ + { + resolve: "medusa-plugin-magento", + options: { + // TODO add options + }, + }, + ], +}) +``` + +You add the plugin to the array of plugins. Later, you'll pass options useful to retrieve data from Magento. + +Finally, to ensure your plugin's changes are constantly published to the local registry, simplifying your testing process, keep the following command running in the plugin project during development: + +```bash title="Plugin project" +npx medusa plugin:develop +``` + +*** + +## Step 4: Implement Magento Module + +To connect to external applications in Medusa, you create a custom module. A module is a reusable package with functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup. + +In this step, you'll create a Magento Module in the Magento plugin that connects to a Magento server's REST APIs and retrieves data, such as products. + +Refer to the [Modules](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) documentation to learn more about modules. + +### Create Module Directory + +A module is created under the `src/modules` directory of your plugin. So, create the directory `src/modules/magento`. + +![Diagram showcasing the module directory to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739272368/magento-1_ikev4x.jpg) + +### Create Module's Service + +You define a module's functionalities in a service. A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to external systems or the database, which is useful if your module defines tables in the database. + +In this section, you'll create the Magento Module's service that connects to Magento's REST APIs and retrieves data. + +Start by creating the file `src/modules/magento/service.ts` in the plugin with the following content: + +![Diagram showcasing the service file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739272483/magento-2_ajetpr.jpg) + +```ts title="src/modules/magento/service.ts" +type Options = { + baseUrl: string + storeCode?: string + username: string + password: string + migrationOptions?: { + imageBaseUrl?: string + } +} + +export default class MagentoModuleService { + private options: Options + + constructor({}, options: Options) { + this.options = { + ...options, + storeCode: options.storeCode || "default", + } + } +} +``` + +You create a `MagentoModuleService` that has an `options` property to store the module's options. These options include: + +- `baseUrl`: The base URL of the Magento server. +- `storeCode`: The store code of the Magento store, which is `default` by default. +- `username`: The username of a Magento admin user to authenticate with the Magento server. +- `password`: The password of the Magento admin user. +- `migrationOptions`: Additional options useful for migrating data, such as the base URL to use for product images. + +The service's constructor accepts as a first parameter the [Module Container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md), which allows you to access resources available for the module. As a second parameter, it accepts the module's options. + +### Add Authentication Logic + +To authenticate with the Magento server, you'll add a method to the service that retrieves an access token from Magento using the username and password in the options. This access token is used in subsequent requests to the Magento server. + +First, add the following property to the `MagentoModuleService` class: + +```ts title="src/modules/magento/service.ts" +export default class MagentoModuleService { + private accessToken: { + token: string + expiresAt: Date + } + // ... +} +``` + +You add an `accessToken` property to store the access token and its expiration date. The access token Magento returns expires after four hours, so you store the expiration date to know when to refresh the token. + +Next, add the following `authenticate` method to the `MagentoModuleService` class: + +```ts title="src/modules/magento/service.ts" +import { MedusaError } from "@medusajs/framework/utils" + +export default class MagentoModuleService { + // ... + async authenticate() { + const response = await fetch(`${this.options.baseUrl}/rest/${this.options.storeCode}/V1/integration/admin/token`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ username: this.options.username, password: this.options.password }), + }) + + const token = await response.text() + + if (!response.ok) { + throw new MedusaError(MedusaError.Types.UNAUTHORIZED, `Failed to authenticate with Magento: ${token}`) + } + + this.accessToken = { + token: token.replaceAll("\"", ""), + expiresAt: new Date(Date.now() + 4 * 60 * 60 * 1000), // 4 hours in milliseconds + } + } +} +``` + +You create an `authenticate` method that sends a POST request to the Magento server's `/rest/{storeCode}/V1/integration/admin/token` endpoint, passing the username and password in the request body. + +If the request is successful, you store the access token and its expiration date in the `accessToken` property. If the request fails, you throw a `MedusaError` with the error message returned by Magento. + +Lastly, add an `isAccessTokenExpired` method that checks if the access token has expired: + +```ts title="src/modules/magento/service.ts" +export default class MagentoModuleService { + // ... + async isAccessTokenExpired(): Promise { + return !this.accessToken || this.accessToken.expiresAt < new Date() + } +} +``` + +In the `isAccessTokenExpired` method, you return a boolean indicating whether the access token has expired. You'll use this in later methods to check if you need to refresh the access token. + +### Retrieve Products from Magento + +Next, you'll add a method that retrieves products from Magento. Due to limitations in Magento's API that makes it difficult to differentiate between simple products that don't belong to a configurable product and those that do, you'll only retrieve configurable products and their children. You'll also retrieve the configurable attributes of the product, such as color and size. + +First, you'll add some types to represent a Magento product and its attributes. Create the file `src/modules/magento/types.ts` in the plugin with the following content: + +![Diagram showcasing the types file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739346287/Medusa%20Resources/magento-3_fpghog.jpg) + +```ts title="src/modules/magento/types.ts" +export type MagentoProduct = { + id: number + sku: string + name: string + price: number + status: number + // not handling other types + type_id: "simple" | "configurable" + created_at: string + updated_at: string + extension_attributes: { + category_links: { + category_id: string + }[] + configurable_product_links?: number[] + configurable_product_options?: { + id: number + attribute_id: string + label: string + position: number + values: { + value_index: number + }[] + }[] + } + media_gallery_entries: { + id: number + media_type: string + label: string + position: number + disabled: boolean + types: string[] + file: string + }[] + custom_attributes: { + attribute_code: string + value: string + }[] + // added by module + children?: MagentoProduct[] +} + +export type MagentoAttribute = { + attribute_code: string + attribute_id: number + default_frontend_label: string + options: { + label: string + value: string + }[] +} + +export type MagentoPagination = { + search_criteria: { + filter_groups: [], + page_size: number + current_page: number + } + total_count: number +} + +export type MagentoPaginatedResponse = { + items: TData[] +} & MagentoPagination +``` + +You define the following types: + +- `MagentoProduct`: Represents a product in Magento. +- `MagentoAttribute`: Represents an attribute in Magento. +- `MagentoPagination`: Represents the pagination information returned by Magento's API. +- `MagentoPaginatedResponse`: Represents a paginated response from Magento's API for a specific item type, such as products. + +Next, add the `getProducts` method to the `MagentoModuleService` class: + +```ts title="src/modules/magento/service.ts" +export default class MagentoModuleService { + // ... + async getProducts(options?: { + currentPage?: number + pageSize?: number + }): Promise<{ + products: MagentoProduct[] + attributes: MagentoAttribute[] + pagination: MagentoPagination + }> { + const { currentPage = 1, pageSize = 100 } = options || {} + const getAccessToken = await this.isAccessTokenExpired() + if (getAccessToken) { + await this.authenticate() + } + + // TODO prepare query params + } +} +``` + +The `getProducts` method receives an optional `options` object with the `currentPage` and `pageSize` properties. So far, you check if the access token has expired and, if so, retrieve a new one using the `authenticate` method. + +Next, you'll prepare the query parameters to pass in the request that retrieves products. Replace the `TODO` with the following: + +```ts title="src/modules/magento/service.ts" +const searchQuery = new URLSearchParams() +// pass pagination parameters +searchQuery.append( + "searchCriteria[currentPage]", + currentPage?.toString() || "1" +) +searchQuery.append( + "searchCriteria[pageSize]", + pageSize?.toString() || "100" +) + +// retrieve only configurable products +searchQuery.append( + "searchCriteria[filter_groups][1][filters][0][field]", + "type_id" +) +searchQuery.append( + "searchCriteria[filter_groups][1][filters][0][value]", + "configurable" +) +searchQuery.append( + "searchCriteria[filter_groups][1][filters][0][condition_type]", + "in" +) + +// TODO send request to retrieve products +``` + +You create a `searchQuery` object to store the query parameters to pass in the request. Then, you add the pagination parameters and the filter to retrieve only configurable products. + +Next, you'll send the request to retrieve products from Magento. Replace the `TODO` with the following: + +```ts title="src/modules/magento/service.ts" +const { items: products, ...pagination }: MagentoPaginatedResponse = await fetch( + `${this.options.baseUrl}/rest/${this.options.storeCode}/V1/products?${searchQuery}`, + { + headers: { + "Authorization": `Bearer ${this.accessToken.token}`, + }, + } +).then((res) => res.json()) +.catch((err) => { + console.log(err) + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Failed to get products from Magento: ${err.message}` + ) +}) + +// TODO prepare products +``` + +You send a `GET` request to the Magento server's `/rest/{storeCode}/V1/products` endpoint, passing the query parameters in the URL. You also pass the access token in the `Authorization` header. + +Next, you'll prepare the retrieved products by retrieving their children, configurable attributes, and modifying their image URLs. Replace the `TODO` with the following: + +```ts title="src/modules/magento/service.ts" +const attributeIds: string[] = [] + +await promiseAll( + products.map(async (product) => { + // retrieve its children + product.children = await fetch( + `${this.options.baseUrl}/rest/${this.options.storeCode}/V1/configurable-products/${product.sku}/children`, + { + headers: { + "Authorization": `Bearer ${this.accessToken.token}`, + }, + } + ).then((res) => res.json()) + .catch((err) => { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Failed to get product children from Magento: ${err.message}` + ) + }) + + product.media_gallery_entries = product.media_gallery_entries.map( + (entry) => ({ + ...entry, + file: `${this.options.migrationOptions?.imageBaseUrl}${entry.file}`, + } + )) + + attributeIds.push(...( + product.extension_attributes.configurable_product_options?.map( + (option) => option.attribute_id) || [] + ) + ) + }) +) + +// TODO retrieve attributes +``` + +You loop over the retrieved products and retrieve their children using the `/rest/{storeCode}/V1/configurable-products/{sku}/children` endpoint. You also modify the image URLs to use the base URL in the migration options, if provided. + +In addition, you store the IDs of the configurable products' attributes in the `attributeIds` array. You'll add a method that retrieves these attributes. + +Add the new method `getAttributes` to the `MagentoModuleService` class: + +```ts title="src/modules/magento/service.ts" +export default class MagentoModuleService { + // ... + async getAttributes({ + ids, + }: { + ids: string[] + }): Promise { + const getAccessToken = await this.isAccessTokenExpired() + if (getAccessToken) { + await this.authenticate() + } + + // filter by attribute IDs + const searchQuery = new URLSearchParams() + searchQuery.append( + "searchCriteria[filter_groups][0][filters][0][field]", + "attribute_id" + ) + searchQuery.append( + "searchCriteria[filter_groups][0][filters][0][value]", + ids.join(",") + ) + searchQuery.append( + "searchCriteria[filter_groups][0][filters][0][condition_type]", + "in" + ) + + const { + items: attributes, + }: MagentoPaginatedResponse = await fetch( + `${this.options.baseUrl}/rest/${this.options.storeCode}/V1/products/attributes?${searchQuery}`, + { + headers: { + "Authorization": `Bearer ${this.accessToken.token}`, + }, + } + ).then((res) => res.json()) + .catch((err) => { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Failed to get attributes from Magento: ${err.message}` + ) + }) + + return attributes + } +} +``` + +The `getAttributes` method receives an object with the `ids` property, which is an array of attribute IDs. You check if the access token has expired and, if so, retrieve a new one using the `authenticate` method. + +Next, you prepare the query parameters to pass in the request to retrieve attributes. You send a `GET` request to the Magento server's `/rest/{storeCode}/V1/products/attributes` endpoint, passing the query parameters in the URL. You also pass the access token in the `Authorization` header. + +Finally, you return the retrieved attributes. + +Now, go back to the `getProducts` method and replace the `TODO` with the following: + +```ts title="src/modules/magento/service.ts" +const attributes = await this.getAttributes({ ids: attributeIds }) + +return { products, attributes, pagination } +``` + +You retrieve the configurable products' attributes using the `getAttributes` method and return the products, attributes, and pagination information. + +You'll use this method in a later step to retrieve products from Magento. + +### Export Module Definition + +The final piece to a module is its definition, which you export in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its service. + +So, create the file `src/modules/magento/index.ts` with the following content: + +![Diagram showcasing the module definition file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739348316/Medusa%20Resources/magento-4_bmepvh.jpg) + +```ts title="src/modules/magento/index.ts" +import { Module } from "@medusajs/framework/utils" +import MagentoModuleService from "./service" + +export const MAGENTO_MODULE = "magento" + +export default Module(MAGENTO_MODULE, { + service: MagentoModuleService, +}) +``` + +You use the `Module` function from the Modules SDK to create the module's definition. It accepts two parameters: + +1. The module's name, which is `magento`. +2. An object with a required property `service` indicating the module's service. + +You'll later use the module's service to retrieve products from Magento. + +### Pass Options to Plugin + +As mentioned earlier when you registered the plugin in the Medusa Application's `medusa-config.ts` file, you can pass options to the plugin. These options are then passed to the modules in the plugin. + +So, add the following options to the plugin's registration in the `medusa-config.ts` file of the Medusa application: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + plugins: [ + { + resolve: "medusa-plugin-magento", + options: { + baseUrl: process.env.MAGENTO_BASE_URL, + username: process.env.MAGENTO_USERNAME, + password: process.env.MAGENTO_PASSWORD, + migrationOptions: { + imageBaseUrl: process.env.MAGENTO_IMAGE_BASE_URL, + }, + }, + }, + ], +}) +``` + +You pass the options that you defined in the `MagentoModuleService`. Make sure to also set their environment variables in the `.env` file: + +```bash +MAGENTO_BASE_URL=https://magento.example.com +MAGENTO_USERNAME=admin +MAGENTO_PASSWORD=password +MAGENTO_IMAGE_BASE_URL=https://magento.example.com/pub/media/catalog/product +``` + +Where: + +- `MAGENTO_BASE_URL`: The base URL of the Magento server. It can also be a local URL, such as `http://localhost:8080`. +- `MAGENTO_USERNAME`: The username of a Magento admin user to authenticate with the Magento server. +- `MAGENTO_PASSWORD`: The password of the Magento admin user. +- `MAGENTO_IMAGE_BASE_URL`: The base URL to use for product images. Magento stores product images in the `pub/media/catalog/product` directory, so you can reference them directly or use a CDN URL. If the URLs of product images in the Medusa server already have a different base URL, you can omit this option. + +Medusa supports integrating third-party services, such as [S3](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/file/s3/index.html.md), in a File Module Provider. Refer to the [File Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/file/index.html.md) documentation to find other module providers and how to create a custom provider. + +You can now use the Magento Module to migrate data, which you'll do in the next steps. + +*** + +## Step 5: Build Product Migration Workflow + +In this section, you'll add the feature to migrate products from Magento to Medusa. To implement this feature, you'll use a workflow. + +A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow like you construct a function, but it's a special function that allows you to track its executions' progress, define roll-back logic, and configure other advanced features. Then, you execute the workflow from other customizations, such as in an API route or a scheduled job. + +By implementing the migration feature in a workflow, you ensure that the data remains consistent and that the migration process can be rolled back if an error occurs. + +Refer to the [Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) documentation to learn more about workflows. + +### Workflow Steps + +The workflow you'll create will have the following steps: + +- [getMagentoProductsStep](#getMagentoProductsStep): Retrieve products from Magento using the Magento Module. +- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve Medusa store details, which you'll need when creating the products. +- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve a shipping profile, which you'll associate the created products with. +- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve Magento products that are already in Medusa to update them, instead of creating them. +- [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/index.html.md): Create products in the Medusa application. +- [updateProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductsWorkflow/index.html.md): Update existing products in the Medusa application. + +You only need to implement the `getMagentoProductsStep` step, which retrieves the products from Magento. The other steps and workflows are provided by Medusa's `@medusajs/medusa/core-flows` package. + +### getMagentoProductsStep + +The first step of the workflow retrieves and returns the products from Magento. + +In your plugin, create the file `src/workflows/steps/get-magento-products.ts` with the following content: + +![Diagram showcasing the get-magento-products file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739349590/Medusa%20Resources/magento-5_ueb4wn.jpg) + +```ts title="src/workflows/steps/get-magento-products.ts" +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +import { MAGENTO_MODULE } from "../../modules/magento" +import MagentoModuleService from "../../modules/magento/service" + +type GetMagentoProductsInput = { + currentPage: number + pageSize: number +} + +export const getMagentoProductsStep = createStep( + "get-magento-products", + async ({ currentPage, pageSize }: GetMagentoProductsInput, { container }) => { + const magentoModuleService: MagentoModuleService = + container.resolve(MAGENTO_MODULE) + + const response = await magentoModuleService.getProducts({ + currentPage, + pageSize, + }) + + return new StepResponse(response) + } +) +``` + +You create a step using `createStep` from the Workflows SDK. It accepts two parameters: + +1. The step's name, which is `get-magento-products`. +2. An async function that executes the step's logic. The function receives two parameters: + - The input data for the step, which in this case is the pagination parameters. + - An object holding the workflow's context, including the [Medusa Container](https://docs.medusajs.com/docslearn/fundamentals/medusa-container/index.html.md) that allows you to resolve Framework and commerce tools. + +In the step function, you resolve the Magento Module's service from the container, then use its `getProducts` method to retrieve the products from Magento. + +Steps that return data must return them in a `StepResponse` instance. The `StepResponse` constructor accepts as a parameter the data to return. + +### Create migrateProductsFromMagentoWorkflow + +You'll now create the workflow that migrates products from Magento using the step you created and steps from Medusa's `@medusajs/medusa/core-flows` package. + +In your plugin, create the file `src/workflows/migrate-products-from-magento.ts` with the following content: + +![Diagram showcasing the migrate-products-from-magento file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739349820/Medusa%20Resources/magento-6_jjdaxj.jpg) + +```ts title="src/workflows/migrate-products-from-magento.ts" +import { + createWorkflow, transform, WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { + CreateProductWorkflowInputDTO, UpsertProductDTO, +} from "@medusajs/framework/types" +import { + createProductsWorkflow, + updateProductsWorkflow, + useQueryGraphStep, +} from "@medusajs/medusa/core-flows" +import { getMagentoProductsStep } from "./steps/get-magento-products" + +type MigrateProductsFromMagentoWorkflowInput = { + currentPage: number + pageSize: number +} + +export const migrateProductsFromMagentoWorkflowId = + "migrate-products-from-magento" + +export const migrateProductsFromMagentoWorkflow = createWorkflow( + { + name: migrateProductsFromMagentoWorkflowId, + retentionTime: 10000, + store: true, + }, + (input: MigrateProductsFromMagentoWorkflowInput) => { + const { pagination, products, attributes } = getMagentoProductsStep( + input + ) + // TODO prepare data to create and update products + } +) +``` + +You create a workflow using `createWorkflow` from the Workflows SDK. It accepts two parameters: + +1. An object with the workflow's configuration, including the name and whether to store the workflow's executions. You enable storing the workflow execution so that you can view it later in the Medusa Admin dashboard. +2. A worflow constructor function, which holds the workflow's implementation. The function receives the input data for the workflow, which is the pagination parameters. + +In the workflow constructor function, you use the `getMagentoProductsStep` step to retrieve the products from Magento, passing it the pagination parameters from the workflow's input. + +Next, you'll retrieve the Medusa store details and shipping profiles. These are necessary to prepare the data of the products to create or update. + +Replace the `TODO` in the workflow function with the following: + +```ts title="src/workflows/migrate-products-from-magento.ts" +const { data: stores } = useQueryGraphStep({ + entity: "store", + fields: ["supported_currencies.*", "default_sales_channel_id"], + pagination: { + take: 1, + skip: 0, + }, +}) + +const { data: shippingProfiles } = useQueryGraphStep({ + entity: "shipping_profile", + fields: ["id"], + pagination: { + take: 1, + skip: 0, + }, +}).config({ name: "get-shipping-profiles" }) + +// TODO retrieve existing products +``` + +You use the `useQueryGraphStep` step to retrieve the store details and shipping profiles. `useQueryGraphStep` is a Medusa step that wraps [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), allowing you to use it in a workflow. Query is a tool that retrieves data across modules. + +Whe retrieving the store details, you specifically retrieve its supported currencies and default sales channel ID. You'll associate the products with the store's default sales channel, and set their variant prices in the supported currencies. You'll also associate the products with a shipping profile. + +Next, you'll retrieve products that were previously migrated from Magento to determine which products to create or update. Replace the `TODO` with the following: + +```ts title="src/workflows/migrate-products-from-magento.ts" +const externalIdFilters = transform({ + products, +}, (data) => { + return data.products.map((product) => product.id.toString()) +}) + +const { data: existingProducts } = useQueryGraphStep({ + entity: "product", + fields: ["id", "external_id", "variants.id", "variants.metadata"], + filters: { + external_id: externalIdFilters, + }, +}).config({ name: "get-existing-products" }) + +// TODO prepare products to create or update +``` + +Since the Medusa application creates an internal representation of the workflow's constructor function, you can't manipulate data directly, as variables have no value while creating the internal representation. + +Refer to the [Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md) documentation to learn more about the workflow constructor function's constraints. + +Instead, you can manipulate data in a workflow's constructor function using `transform` from the Workflows SDK. `transform` is a function that accepts two parameters: + +- The data to transform, which in this case is the Magento products. +- A function that transforms the data. The function receives the data passed in the first parameter and returns the transformed data. + +In the transformation function, you return the IDs of the Magento products. Then, you use the `useQueryGraphStep` to retrieve products in the Medusa application that have an `external_id` property matching the IDs of the Magento products. You'll use this property to store the IDs of the products in Magento. + +Next, you'll prepare the data to create and update the products. Replace the `TODO` in the workflow function with the following: + +```ts title="src/workflows/migrate-products-from-magento.ts" highlights={prepareHighlights} +const { + productsToCreate, + productsToUpdate, +} = transform({ + products, + attributes, + stores, + shippingProfiles, + existingProducts, +}, (data) => { + const productsToCreate = new Map() + const productsToUpdate = new Map() + + data.products.forEach((magentoProduct) => { + const productData: CreateProductWorkflowInputDTO | UpsertProductDTO = { + title: magentoProduct.name, + description: magentoProduct.custom_attributes.find( + (attr) => attr.attribute_code === "description" + )?.value, + status: magentoProduct.status === 1 ? "published" : "draft", + handle: magentoProduct.custom_attributes.find( + (attr) => attr.attribute_code === "url_key" + )?.value, + external_id: magentoProduct.id.toString(), + thumbnail: magentoProduct.media_gallery_entries.find( + (entry) => entry.types.includes("thumbnail") + )?.file, + sales_channels: [{ + id: data.stores[0].default_sales_channel_id, + }], + shipping_profile_id: data.shippingProfiles[0].id, + } + const existingProduct = data.existingProducts.find((p) => p.external_id === productData.external_id) + + if (existingProduct) { + productData.id = existingProduct.id + } + + productData.options = magentoProduct.extension_attributes.configurable_product_options?.map((option) => { + const attribute = data.attributes.find((attr) => attr.attribute_id === parseInt(option.attribute_id)) + return { + title: option.label, + values: attribute?.options.filter((opt) => { + return option.values.find((v) => v.value_index === parseInt(opt.value)) + }).map((opt) => opt.label) || [], + } + }) || [] + + productData.variants = magentoProduct.children?.map((child) => { + const childOptions: Record = {} + + child.custom_attributes.forEach((attr) => { + const attrData = data.attributes.find((a) => a.attribute_code === attr.attribute_code) + if (!attrData) { + return + } + + childOptions[attrData.default_frontend_label] = attrData.options.find((opt) => opt.value === attr.value)?.label || "" + }) + + const variantExternalId = child.id.toString() + const existingVariant = existingProduct.variants.find((v) => v.metadata.external_id === variantExternalId) + + return { + title: child.name, + sku: child.sku, + options: childOptions, + prices: data.stores[0].supported_currencies.map(({ currency_code }) => { + return { + amount: child.price, + currency_code, + } + }), + metadata: { + external_id: variantExternalId, + }, + id: existingVariant?.id, + } + }) + + productData.images = magentoProduct.media_gallery_entries.filter((entry) => !entry.types.includes("thumbnail")).map((entry) => { + return { + url: entry.file, + metadata: { + external_id: entry.id.toString(), + }, + } + }) + + if (productData.id) { + productsToUpdate.set(existingProduct.id, productData) + } else { + productsToCreate.set(productData.external_id!, productData) + } + }) + + return { + productsToCreate: Array.from(productsToCreate.values()), + productsToUpdate: Array.from(productsToUpdate.values()), + } +}) + +// TODO create and update products +``` + +You use `transform` again to prepare the data to create and update the products in the Medusa application. For each Magento product, you map its equivalent Medusa product's data: + +- You set the product's general details, such as the title, description, status, handle, external ID, and thumbnail using the Magento product's data and custom attributes. +- You associate the product with the default sales channel and shipping profile retrieved previously. +- You map the Magento product's configurable product options to Medusa product options. In Medusa, a product's option has a label, such as "Color", and values, such as "Red". To map the option values, you use the attributes retrieved from Magento. +- You map the Magento product's children to Medusa product variants. For the variant options, you pass an object whose keys is the option's label, such as "Color", and values is the option's value, such as "Red". For the prices, you set the variant's price based on the Magento child's price for every supported currency in the Medusa store. Also, you set the Magento child product's ID in the Medusa variant's `metadata.external_id` property. +- You map the Magento product's media gallery entries to Medusa product images. You filter out the thumbnail image and set the URL and the Magento image's ID in the Medusa image's `metadata.external_id` property. + +In addition, you use the existing products retrieved in the previous step to determine whether a product should be created or updated. If there's an existing product whose `external_id` matches the ID of the magento product, you set the existing product's ID in the `id` property of the product to be updated. You also do the same for its variants. + +Finally, you return the products to create and update. + +The last steps of the workflow is to create and update the products. Replace the `TODO` in the workflow function with the following: + +```ts title="src/workflows/migrate-products-from-magento.ts" +createProductsWorkflow.runAsStep({ + input: { + products: productsToCreate, + }, +}) + +updateProductsWorkflow.runAsStep({ + input: { + products: productsToUpdate, + }, +}) + +return new WorkflowResponse(pagination) +``` + +You use the `createProductsWorkflow` and `updateProductsWorkflow` workflows from Medusa's `@medusajs/medusa/core-flows` package to create and update the products in the Medusa application. + +Workflows must return an instance of `WorkflowResponse`, passing as a parameter the data to return to the workflow's executor. This workflow returns the pagination parameters, allowing you to paginate the product migration process. + +You can now use this workflow to migrate products from Magento to Medusa. You'll learn how to use it in the next steps. + +*** + +## Step 6: Schedule Product Migration + +There are many ways to execute tasks asynchronously in Medusa, such as [scheduling a job](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md) or [handling emitted events](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md). + +In this guide, you'll learn how to schedule the product migration at a specified interval using a scheduled job. A scheduled job is an asynchronous function that the Medusa application runs at the interval you specify during the Medusa application's runtime. + +Refer to the [Scheduled Jobs](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md) documentation to learn more about scheduled jobs. + +To create a scheduled job, in your plugin, create the file `src/jobs/migrate-magento.ts` with the following content: + +![Diagram showcasing the migrate-magento file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739358924/Medusa%20Resources/magento-7_rqoodo.jpg) + +```ts title="src/jobs/migrate-magento.ts" +import { MedusaContainer } from "@medusajs/framework/types" +import { migrateProductsFromMagentoWorkflow } from "../workflows" + +export default async function migrateMagentoJob( + container: MedusaContainer +) { + const logger = container.resolve("logger") + logger.info("Migrating products from Magento...") + + let currentPage = 0 + const pageSize = 100 + let totalCount = 0 + + do { + currentPage++ + + const { + result: pagination, + } = await migrateProductsFromMagentoWorkflow(container).run({ + input: { + currentPage, + pageSize, + }, + }) + + totalCount = pagination.total_count + } while (currentPage * pageSize < totalCount) + + logger.info("Finished migrating products from Magento") +} + +export const config = { + name: "migrate-magento-job", + schedule: "0 0 * * *", +} +``` + +A scheduled job file must export: + +- An asynchronous function that executes the job's logic. The function receives the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) as a parameter. +- An object with the job's configuration, including the name and the schedule. The schedule is a cron job pattern as a string. + +In the job function, you resolve the [logger](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md) from the container to log messages. Then, you paginate the product migration process by running the `migrateProductsFromMagentoWorkflow` workflow at each page until you've migrated all products. You use the pagination result returned by the workflow to determine whether there are more products to migrate. + +Based on the job's configurations, the Medusa application will run the job at midnight every day. + +### Test it Out + +To test out this scheduled job, first, change the configuration to run the job every minute: + +```ts title="src/jobs/migrate-magento.ts" +export const config = { + // ... + schedule: "* * * * *", +} +``` + +Then, make sure to run the `plugin:develop` command in the plugin if you haven't already: + +```bash +npx medusa plugin:develop +``` + +This ensures that the plugin's latest changes are reflected in the Medusa application. + +Finally, start the Medusa application that the plugin is installed in: + +```bash npm2yarn +npm run dev +``` + +After a minute, you'll see a message in the terminal indicating that the migration started: + +```plain title="Terminal" +info: Migrating products from Magento... +``` + +Once the migration is done, you'll see the following message: + +```plain title="Terminal" +info: Finished migrating products from Magento +``` + +To confirm that the products were migrated, open the Medusa Admin dashboard at `http://localhost:9000/app` and log in. Then, click on Products in the sidebar. You'll see your magento products in the list of products. + +![Click on products at the sidebar on the right, then view the products in the table in the middle.](https://res.cloudinary.com/dza7lstvk/image/upload/v1739359394/Medusa%20Resources/Screenshot_2025-02-12_at_1.22.44_PM_uva98i.png) + +*** + +## Next Steps + +You've now implemented the logic to migrate products from Magento to Medusa. You can re-use the plugin across Medusa applications. You can also expand on the plugin to: + +- Migrate other entities, such as orders, customers, and categories. Migrating other entities follows the same pattern as migrating products, using workflows and scheduled jobs. You only need to format the data to be migrated as needed. +- Allow triggering migrations from the Medusa Admin dashboard using [Admin Customizations](https://docs.medusajs.com/docs/learn/fundamentals/admin/index.html.md). This feature is available in the [Example Repository](https://github.com/medusajs/example-repository/tree/main/src/admin). + +If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth learning of all the concepts you've used in this guide and more. + +To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md). + + # Implement Localization in Medusa by Integrating Contentful In this tutorial, you'll learn how to localize your Medusa store's data with Contentful. @@ -55443,1054 +56556,6 @@ If you encounter issues not covered in the troubleshooting guides: 3. Contact the [sales team](https://medusajs.com/contact/) to get help from the Medusa team. -# How to Build Magento Data Migration Plugin - -In this tutorial, you'll learn how to build a [plugin](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md) that migrates data, such as products, from Magento to Medusa. - -Magento is known for its customization capabilities. However, its monolithic architecture imposes limitations on business requirements, often forcing development teams to implement hacky workarounds. Over time, these customizations become challenging to maintain, especially as the business scales, leading to increased technical debt and slower feature delivery. - -Medusa's modular architecture allows you to build a custom digital commerce platform that meets your business requirements without the limitations of a monolithic system. By migrating from Magento to Medusa, you can take advantage of Medusa's modern technology stack to build a scalable and flexible commerce platform that grows with your business. - -By following this tutorial, you'll create a Medusa plugin that migrates data from a Magento server to a Medusa application in minimal time. You can re-use this plugin across multiple Medusa applications, allowing you to adopt Medusa across your projects. - -## Summary - -### Prerequisites - - - -This tutorial will teach you how to: - -- Install and set up a Medusa application project. -- Install and set up a Medusa plugin. -- Implement a Magento Module in the plugin to connect to Magento's APIs and retrieve products. - - This guide will only focus on migrating product data from Magento to Medusa. You can extend the implementation to migrate other data, such as customers, orders, and more. -- Trigger data migration from Magento to Medusa in a scheduled job. - -You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer. - -![Diagram showcasing the flow of migrating data from Magento to Medusa](https://res.cloudinary.com/dza7lstvk/image/upload/v1739360550/Medusa%20Resources/magento-summary_hsewci.jpg) - -[Example Repository](https://github.com/medusajs/examples/tree/main/migrate-from-magento): Find the full code of the guide in this repository. The repository also includes additional features, such as triggering migrations from the Medusa Admin dashboard. - -*** - -## Step 1: Install a Medusa Application - -You'll first install a Medusa application that exposes core commerce features through REST APIs. You'll later install the Magento plugin in this application to test it out. - -### Prerequisites - -- [Node.js v20+](https://nodejs.org/en/download) -- [Git CLI tool](https://git-scm.com/downloads) -- [PostgreSQL](https://www.postgresql.org/download/) - -Start by installing the Medusa application on your machine with the following command: - -```bash -npx create-medusa-app@latest -``` - -You'll be asked for the project's name. You can also optionally choose to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md). - -Afterward, the installation process will start, which will install the Medusa application in a directory with your project's name. If you chose to install the Next.js starter, it'll be installed in a separate directory with the `{project-name}-storefront` name. - -The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Refer to the [Medusa Architecture](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md) documentation to learn more. - -Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterward, you can log in with the new user and explore the dashboard. - -Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help. - -*** - -## Step 2: Install a Medusa Plugin Project - -A plugin is a package of reusable Medusa customizations that you can install in any Medusa application. You can add in the plugin [API Routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md), [Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), and other customizations, as you'll see in this guide. Afterward, you can test it out locally in a Medusa application, then publish it to npm to install and use it in any Medusa application. - -Refer to the [Plugins](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md) documentation to learn more about plugins. - -A Medusa plugin is set up in a different project, giving you the flexibility in building and publishing it, while providing you with the tools to test it out locally in a Medusa application. - -To create a new Medusa plugin project, run the following command in a directory different than that of the Medusa application: - -```bash npm2yarn -npx create-medusa-app@latest medusa-plugin-magento --plugin -``` - -Where `medusa-plugin-magento` is the name of the plugin's directory and the name set in the plugin's `package.json`. So, if you wish to publish it to NPM later under a different name, you can change it here in the command or later in `package.json`. - -Once the installation process is done, a new directory named `medusa-plugin-magento` will be created with the plugin project files. - -![Directory structure of a plugin project](https://res.cloudinary.com/dza7lstvk/image/upload/v1737019441/Medusa%20Book/project-dir_q4xtri.jpg) - -*** - -## Step 3: Set up Plugin in Medusa Application - -Before you start your development, you'll set up the plugin in the Medusa application you installed in the first step. This will allow you to test the plugin during your development process. - -In the plugin's directory, run the following command to publish the plugin to the local package registry: - -```bash title="Plugin project" -npx medusa plugin:publish -``` - -This command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. The plugin is published locally under the name you specified in `package.json`. - -Next, you'll install the plugin in the Medusa application from the local registry. - -If you've installed your Medusa project before v2.3.1, you must install [yalc](https://github.com/wclr/yalc) as a development dependency first. - -Run the following command in the Medusa application's directory to install the plugin: - -```bash title="Medusa application" -npx medusa plugin:add medusa-plugin-magento -``` - -This command installs the plugin in the Medusa application from the local package registry. - -Next, register the plugin in the `medusa-config.ts` file of the Medusa application: - -```ts title="medusa-config.ts" -module.exports = defineConfig({ - // ... - plugins: [ - { - resolve: "medusa-plugin-magento", - options: { - // TODO add options - }, - }, - ], -}) -``` - -You add the plugin to the array of plugins. Later, you'll pass options useful to retrieve data from Magento. - -Finally, to ensure your plugin's changes are constantly published to the local registry, simplifying your testing process, keep the following command running in the plugin project during development: - -```bash title="Plugin project" -npx medusa plugin:develop -``` - -*** - -## Step 4: Implement Magento Module - -To connect to external applications in Medusa, you create a custom module. A module is a reusable package with functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup. - -In this step, you'll create a Magento Module in the Magento plugin that connects to a Magento server's REST APIs and retrieves data, such as products. - -Refer to the [Modules](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) documentation to learn more about modules. - -### Create Module Directory - -A module is created under the `src/modules` directory of your plugin. So, create the directory `src/modules/magento`. - -![Diagram showcasing the module directory to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739272368/magento-1_ikev4x.jpg) - -### Create Module's Service - -You define a module's functionalities in a service. A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to external systems or the database, which is useful if your module defines tables in the database. - -In this section, you'll create the Magento Module's service that connects to Magento's REST APIs and retrieves data. - -Start by creating the file `src/modules/magento/service.ts` in the plugin with the following content: - -![Diagram showcasing the service file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739272483/magento-2_ajetpr.jpg) - -```ts title="src/modules/magento/service.ts" -type Options = { - baseUrl: string - storeCode?: string - username: string - password: string - migrationOptions?: { - imageBaseUrl?: string - } -} - -export default class MagentoModuleService { - private options: Options - - constructor({}, options: Options) { - this.options = { - ...options, - storeCode: options.storeCode || "default", - } - } -} -``` - -You create a `MagentoModuleService` that has an `options` property to store the module's options. These options include: - -- `baseUrl`: The base URL of the Magento server. -- `storeCode`: The store code of the Magento store, which is `default` by default. -- `username`: The username of a Magento admin user to authenticate with the Magento server. -- `password`: The password of the Magento admin user. -- `migrationOptions`: Additional options useful for migrating data, such as the base URL to use for product images. - -The service's constructor accepts as a first parameter the [Module Container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md), which allows you to access resources available for the module. As a second parameter, it accepts the module's options. - -### Add Authentication Logic - -To authenticate with the Magento server, you'll add a method to the service that retrieves an access token from Magento using the username and password in the options. This access token is used in subsequent requests to the Magento server. - -First, add the following property to the `MagentoModuleService` class: - -```ts title="src/modules/magento/service.ts" -export default class MagentoModuleService { - private accessToken: { - token: string - expiresAt: Date - } - // ... -} -``` - -You add an `accessToken` property to store the access token and its expiration date. The access token Magento returns expires after four hours, so you store the expiration date to know when to refresh the token. - -Next, add the following `authenticate` method to the `MagentoModuleService` class: - -```ts title="src/modules/magento/service.ts" -import { MedusaError } from "@medusajs/framework/utils" - -export default class MagentoModuleService { - // ... - async authenticate() { - const response = await fetch(`${this.options.baseUrl}/rest/${this.options.storeCode}/V1/integration/admin/token`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ username: this.options.username, password: this.options.password }), - }) - - const token = await response.text() - - if (!response.ok) { - throw new MedusaError(MedusaError.Types.UNAUTHORIZED, `Failed to authenticate with Magento: ${token}`) - } - - this.accessToken = { - token: token.replaceAll("\"", ""), - expiresAt: new Date(Date.now() + 4 * 60 * 60 * 1000), // 4 hours in milliseconds - } - } -} -``` - -You create an `authenticate` method that sends a POST request to the Magento server's `/rest/{storeCode}/V1/integration/admin/token` endpoint, passing the username and password in the request body. - -If the request is successful, you store the access token and its expiration date in the `accessToken` property. If the request fails, you throw a `MedusaError` with the error message returned by Magento. - -Lastly, add an `isAccessTokenExpired` method that checks if the access token has expired: - -```ts title="src/modules/magento/service.ts" -export default class MagentoModuleService { - // ... - async isAccessTokenExpired(): Promise { - return !this.accessToken || this.accessToken.expiresAt < new Date() - } -} -``` - -In the `isAccessTokenExpired` method, you return a boolean indicating whether the access token has expired. You'll use this in later methods to check if you need to refresh the access token. - -### Retrieve Products from Magento - -Next, you'll add a method that retrieves products from Magento. Due to limitations in Magento's API that makes it difficult to differentiate between simple products that don't belong to a configurable product and those that do, you'll only retrieve configurable products and their children. You'll also retrieve the configurable attributes of the product, such as color and size. - -First, you'll add some types to represent a Magento product and its attributes. Create the file `src/modules/magento/types.ts` in the plugin with the following content: - -![Diagram showcasing the types file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739346287/Medusa%20Resources/magento-3_fpghog.jpg) - -```ts title="src/modules/magento/types.ts" -export type MagentoProduct = { - id: number - sku: string - name: string - price: number - status: number - // not handling other types - type_id: "simple" | "configurable" - created_at: string - updated_at: string - extension_attributes: { - category_links: { - category_id: string - }[] - configurable_product_links?: number[] - configurable_product_options?: { - id: number - attribute_id: string - label: string - position: number - values: { - value_index: number - }[] - }[] - } - media_gallery_entries: { - id: number - media_type: string - label: string - position: number - disabled: boolean - types: string[] - file: string - }[] - custom_attributes: { - attribute_code: string - value: string - }[] - // added by module - children?: MagentoProduct[] -} - -export type MagentoAttribute = { - attribute_code: string - attribute_id: number - default_frontend_label: string - options: { - label: string - value: string - }[] -} - -export type MagentoPagination = { - search_criteria: { - filter_groups: [], - page_size: number - current_page: number - } - total_count: number -} - -export type MagentoPaginatedResponse = { - items: TData[] -} & MagentoPagination -``` - -You define the following types: - -- `MagentoProduct`: Represents a product in Magento. -- `MagentoAttribute`: Represents an attribute in Magento. -- `MagentoPagination`: Represents the pagination information returned by Magento's API. -- `MagentoPaginatedResponse`: Represents a paginated response from Magento's API for a specific item type, such as products. - -Next, add the `getProducts` method to the `MagentoModuleService` class: - -```ts title="src/modules/magento/service.ts" -export default class MagentoModuleService { - // ... - async getProducts(options?: { - currentPage?: number - pageSize?: number - }): Promise<{ - products: MagentoProduct[] - attributes: MagentoAttribute[] - pagination: MagentoPagination - }> { - const { currentPage = 1, pageSize = 100 } = options || {} - const getAccessToken = await this.isAccessTokenExpired() - if (getAccessToken) { - await this.authenticate() - } - - // TODO prepare query params - } -} -``` - -The `getProducts` method receives an optional `options` object with the `currentPage` and `pageSize` properties. So far, you check if the access token has expired and, if so, retrieve a new one using the `authenticate` method. - -Next, you'll prepare the query parameters to pass in the request that retrieves products. Replace the `TODO` with the following: - -```ts title="src/modules/magento/service.ts" -const searchQuery = new URLSearchParams() -// pass pagination parameters -searchQuery.append( - "searchCriteria[currentPage]", - currentPage?.toString() || "1" -) -searchQuery.append( - "searchCriteria[pageSize]", - pageSize?.toString() || "100" -) - -// retrieve only configurable products -searchQuery.append( - "searchCriteria[filter_groups][1][filters][0][field]", - "type_id" -) -searchQuery.append( - "searchCriteria[filter_groups][1][filters][0][value]", - "configurable" -) -searchQuery.append( - "searchCriteria[filter_groups][1][filters][0][condition_type]", - "in" -) - -// TODO send request to retrieve products -``` - -You create a `searchQuery` object to store the query parameters to pass in the request. Then, you add the pagination parameters and the filter to retrieve only configurable products. - -Next, you'll send the request to retrieve products from Magento. Replace the `TODO` with the following: - -```ts title="src/modules/magento/service.ts" -const { items: products, ...pagination }: MagentoPaginatedResponse = await fetch( - `${this.options.baseUrl}/rest/${this.options.storeCode}/V1/products?${searchQuery}`, - { - headers: { - "Authorization": `Bearer ${this.accessToken.token}`, - }, - } -).then((res) => res.json()) -.catch((err) => { - console.log(err) - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Failed to get products from Magento: ${err.message}` - ) -}) - -// TODO prepare products -``` - -You send a `GET` request to the Magento server's `/rest/{storeCode}/V1/products` endpoint, passing the query parameters in the URL. You also pass the access token in the `Authorization` header. - -Next, you'll prepare the retrieved products by retrieving their children, configurable attributes, and modifying their image URLs. Replace the `TODO` with the following: - -```ts title="src/modules/magento/service.ts" -const attributeIds: string[] = [] - -await promiseAll( - products.map(async (product) => { - // retrieve its children - product.children = await fetch( - `${this.options.baseUrl}/rest/${this.options.storeCode}/V1/configurable-products/${product.sku}/children`, - { - headers: { - "Authorization": `Bearer ${this.accessToken.token}`, - }, - } - ).then((res) => res.json()) - .catch((err) => { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Failed to get product children from Magento: ${err.message}` - ) - }) - - product.media_gallery_entries = product.media_gallery_entries.map( - (entry) => ({ - ...entry, - file: `${this.options.migrationOptions?.imageBaseUrl}${entry.file}`, - } - )) - - attributeIds.push(...( - product.extension_attributes.configurable_product_options?.map( - (option) => option.attribute_id) || [] - ) - ) - }) -) - -// TODO retrieve attributes -``` - -You loop over the retrieved products and retrieve their children using the `/rest/{storeCode}/V1/configurable-products/{sku}/children` endpoint. You also modify the image URLs to use the base URL in the migration options, if provided. - -In addition, you store the IDs of the configurable products' attributes in the `attributeIds` array. You'll add a method that retrieves these attributes. - -Add the new method `getAttributes` to the `MagentoModuleService` class: - -```ts title="src/modules/magento/service.ts" -export default class MagentoModuleService { - // ... - async getAttributes({ - ids, - }: { - ids: string[] - }): Promise { - const getAccessToken = await this.isAccessTokenExpired() - if (getAccessToken) { - await this.authenticate() - } - - // filter by attribute IDs - const searchQuery = new URLSearchParams() - searchQuery.append( - "searchCriteria[filter_groups][0][filters][0][field]", - "attribute_id" - ) - searchQuery.append( - "searchCriteria[filter_groups][0][filters][0][value]", - ids.join(",") - ) - searchQuery.append( - "searchCriteria[filter_groups][0][filters][0][condition_type]", - "in" - ) - - const { - items: attributes, - }: MagentoPaginatedResponse = await fetch( - `${this.options.baseUrl}/rest/${this.options.storeCode}/V1/products/attributes?${searchQuery}`, - { - headers: { - "Authorization": `Bearer ${this.accessToken.token}`, - }, - } - ).then((res) => res.json()) - .catch((err) => { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Failed to get attributes from Magento: ${err.message}` - ) - }) - - return attributes - } -} -``` - -The `getAttributes` method receives an object with the `ids` property, which is an array of attribute IDs. You check if the access token has expired and, if so, retrieve a new one using the `authenticate` method. - -Next, you prepare the query parameters to pass in the request to retrieve attributes. You send a `GET` request to the Magento server's `/rest/{storeCode}/V1/products/attributes` endpoint, passing the query parameters in the URL. You also pass the access token in the `Authorization` header. - -Finally, you return the retrieved attributes. - -Now, go back to the `getProducts` method and replace the `TODO` with the following: - -```ts title="src/modules/magento/service.ts" -const attributes = await this.getAttributes({ ids: attributeIds }) - -return { products, attributes, pagination } -``` - -You retrieve the configurable products' attributes using the `getAttributes` method and return the products, attributes, and pagination information. - -You'll use this method in a later step to retrieve products from Magento. - -### Export Module Definition - -The final piece to a module is its definition, which you export in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its service. - -So, create the file `src/modules/magento/index.ts` with the following content: - -![Diagram showcasing the module definition file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739348316/Medusa%20Resources/magento-4_bmepvh.jpg) - -```ts title="src/modules/magento/index.ts" -import { Module } from "@medusajs/framework/utils" -import MagentoModuleService from "./service" - -export const MAGENTO_MODULE = "magento" - -export default Module(MAGENTO_MODULE, { - service: MagentoModuleService, -}) -``` - -You use the `Module` function from the Modules SDK to create the module's definition. It accepts two parameters: - -1. The module's name, which is `magento`. -2. An object with a required property `service` indicating the module's service. - -You'll later use the module's service to retrieve products from Magento. - -### Pass Options to Plugin - -As mentioned earlier when you registered the plugin in the Medusa Application's `medusa-config.ts` file, you can pass options to the plugin. These options are then passed to the modules in the plugin. - -So, add the following options to the plugin's registration in the `medusa-config.ts` file of the Medusa application: - -```ts title="medusa-config.ts" -module.exports = defineConfig({ - // ... - plugins: [ - { - resolve: "medusa-plugin-magento", - options: { - baseUrl: process.env.MAGENTO_BASE_URL, - username: process.env.MAGENTO_USERNAME, - password: process.env.MAGENTO_PASSWORD, - migrationOptions: { - imageBaseUrl: process.env.MAGENTO_IMAGE_BASE_URL, - }, - }, - }, - ], -}) -``` - -You pass the options that you defined in the `MagentoModuleService`. Make sure to also set their environment variables in the `.env` file: - -```bash -MAGENTO_BASE_URL=https://magento.example.com -MAGENTO_USERNAME=admin -MAGENTO_PASSWORD=password -MAGENTO_IMAGE_BASE_URL=https://magento.example.com/pub/media/catalog/product -``` - -Where: - -- `MAGENTO_BASE_URL`: The base URL of the Magento server. It can also be a local URL, such as `http://localhost:8080`. -- `MAGENTO_USERNAME`: The username of a Magento admin user to authenticate with the Magento server. -- `MAGENTO_PASSWORD`: The password of the Magento admin user. -- `MAGENTO_IMAGE_BASE_URL`: The base URL to use for product images. Magento stores product images in the `pub/media/catalog/product` directory, so you can reference them directly or use a CDN URL. If the URLs of product images in the Medusa server already have a different base URL, you can omit this option. - -Medusa supports integrating third-party services, such as [S3](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/file/s3/index.html.md), in a File Module Provider. Refer to the [File Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/file/index.html.md) documentation to find other module providers and how to create a custom provider. - -You can now use the Magento Module to migrate data, which you'll do in the next steps. - -*** - -## Step 5: Build Product Migration Workflow - -In this section, you'll add the feature to migrate products from Magento to Medusa. To implement this feature, you'll use a workflow. - -A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow like you construct a function, but it's a special function that allows you to track its executions' progress, define roll-back logic, and configure other advanced features. Then, you execute the workflow from other customizations, such as in an API route or a scheduled job. - -By implementing the migration feature in a workflow, you ensure that the data remains consistent and that the migration process can be rolled back if an error occurs. - -Refer to the [Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) documentation to learn more about workflows. - -### Workflow Steps - -The workflow you'll create will have the following steps: - -- [getMagentoProductsStep](#getMagentoProductsStep): Retrieve products from Magento using the Magento Module. -- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve Medusa store details, which you'll need when creating the products. -- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve a shipping profile, which you'll associate the created products with. -- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve Magento products that are already in Medusa to update them, instead of creating them. -- [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/index.html.md): Create products in the Medusa application. -- [updateProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductsWorkflow/index.html.md): Update existing products in the Medusa application. - -You only need to implement the `getMagentoProductsStep` step, which retrieves the products from Magento. The other steps and workflows are provided by Medusa's `@medusajs/medusa/core-flows` package. - -### getMagentoProductsStep - -The first step of the workflow retrieves and returns the products from Magento. - -In your plugin, create the file `src/workflows/steps/get-magento-products.ts` with the following content: - -![Diagram showcasing the get-magento-products file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739349590/Medusa%20Resources/magento-5_ueb4wn.jpg) - -```ts title="src/workflows/steps/get-magento-products.ts" -import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" -import { MAGENTO_MODULE } from "../../modules/magento" -import MagentoModuleService from "../../modules/magento/service" - -type GetMagentoProductsInput = { - currentPage: number - pageSize: number -} - -export const getMagentoProductsStep = createStep( - "get-magento-products", - async ({ currentPage, pageSize }: GetMagentoProductsInput, { container }) => { - const magentoModuleService: MagentoModuleService = - container.resolve(MAGENTO_MODULE) - - const response = await magentoModuleService.getProducts({ - currentPage, - pageSize, - }) - - return new StepResponse(response) - } -) -``` - -You create a step using `createStep` from the Workflows SDK. It accepts two parameters: - -1. The step's name, which is `get-magento-products`. -2. An async function that executes the step's logic. The function receives two parameters: - - The input data for the step, which in this case is the pagination parameters. - - An object holding the workflow's context, including the [Medusa Container](https://docs.medusajs.com/docslearn/fundamentals/medusa-container/index.html.md) that allows you to resolve Framework and commerce tools. - -In the step function, you resolve the Magento Module's service from the container, then use its `getProducts` method to retrieve the products from Magento. - -Steps that return data must return them in a `StepResponse` instance. The `StepResponse` constructor accepts as a parameter the data to return. - -### Create migrateProductsFromMagentoWorkflow - -You'll now create the workflow that migrates products from Magento using the step you created and steps from Medusa's `@medusajs/medusa/core-flows` package. - -In your plugin, create the file `src/workflows/migrate-products-from-magento.ts` with the following content: - -![Diagram showcasing the migrate-products-from-magento file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739349820/Medusa%20Resources/magento-6_jjdaxj.jpg) - -```ts title="src/workflows/migrate-products-from-magento.ts" -import { - createWorkflow, transform, WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" -import { - CreateProductWorkflowInputDTO, UpsertProductDTO, -} from "@medusajs/framework/types" -import { - createProductsWorkflow, - updateProductsWorkflow, - useQueryGraphStep, -} from "@medusajs/medusa/core-flows" -import { getMagentoProductsStep } from "./steps/get-magento-products" - -type MigrateProductsFromMagentoWorkflowInput = { - currentPage: number - pageSize: number -} - -export const migrateProductsFromMagentoWorkflowId = - "migrate-products-from-magento" - -export const migrateProductsFromMagentoWorkflow = createWorkflow( - { - name: migrateProductsFromMagentoWorkflowId, - retentionTime: 10000, - store: true, - }, - (input: MigrateProductsFromMagentoWorkflowInput) => { - const { pagination, products, attributes } = getMagentoProductsStep( - input - ) - // TODO prepare data to create and update products - } -) -``` - -You create a workflow using `createWorkflow` from the Workflows SDK. It accepts two parameters: - -1. An object with the workflow's configuration, including the name and whether to store the workflow's executions. You enable storing the workflow execution so that you can view it later in the Medusa Admin dashboard. -2. A worflow constructor function, which holds the workflow's implementation. The function receives the input data for the workflow, which is the pagination parameters. - -In the workflow constructor function, you use the `getMagentoProductsStep` step to retrieve the products from Magento, passing it the pagination parameters from the workflow's input. - -Next, you'll retrieve the Medusa store details and shipping profiles. These are necessary to prepare the data of the products to create or update. - -Replace the `TODO` in the workflow function with the following: - -```ts title="src/workflows/migrate-products-from-magento.ts" -const { data: stores } = useQueryGraphStep({ - entity: "store", - fields: ["supported_currencies.*", "default_sales_channel_id"], - pagination: { - take: 1, - skip: 0, - }, -}) - -const { data: shippingProfiles } = useQueryGraphStep({ - entity: "shipping_profile", - fields: ["id"], - pagination: { - take: 1, - skip: 0, - }, -}).config({ name: "get-shipping-profiles" }) - -// TODO retrieve existing products -``` - -You use the `useQueryGraphStep` step to retrieve the store details and shipping profiles. `useQueryGraphStep` is a Medusa step that wraps [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), allowing you to use it in a workflow. Query is a tool that retrieves data across modules. - -Whe retrieving the store details, you specifically retrieve its supported currencies and default sales channel ID. You'll associate the products with the store's default sales channel, and set their variant prices in the supported currencies. You'll also associate the products with a shipping profile. - -Next, you'll retrieve products that were previously migrated from Magento to determine which products to create or update. Replace the `TODO` with the following: - -```ts title="src/workflows/migrate-products-from-magento.ts" -const externalIdFilters = transform({ - products, -}, (data) => { - return data.products.map((product) => product.id.toString()) -}) - -const { data: existingProducts } = useQueryGraphStep({ - entity: "product", - fields: ["id", "external_id", "variants.id", "variants.metadata"], - filters: { - external_id: externalIdFilters, - }, -}).config({ name: "get-existing-products" }) - -// TODO prepare products to create or update -``` - -Since the Medusa application creates an internal representation of the workflow's constructor function, you can't manipulate data directly, as variables have no value while creating the internal representation. - -Refer to the [Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md) documentation to learn more about the workflow constructor function's constraints. - -Instead, you can manipulate data in a workflow's constructor function using `transform` from the Workflows SDK. `transform` is a function that accepts two parameters: - -- The data to transform, which in this case is the Magento products. -- A function that transforms the data. The function receives the data passed in the first parameter and returns the transformed data. - -In the transformation function, you return the IDs of the Magento products. Then, you use the `useQueryGraphStep` to retrieve products in the Medusa application that have an `external_id` property matching the IDs of the Magento products. You'll use this property to store the IDs of the products in Magento. - -Next, you'll prepare the data to create and update the products. Replace the `TODO` in the workflow function with the following: - -```ts title="src/workflows/migrate-products-from-magento.ts" highlights={prepareHighlights} -const { - productsToCreate, - productsToUpdate, -} = transform({ - products, - attributes, - stores, - shippingProfiles, - existingProducts, -}, (data) => { - const productsToCreate = new Map() - const productsToUpdate = new Map() - - data.products.forEach((magentoProduct) => { - const productData: CreateProductWorkflowInputDTO | UpsertProductDTO = { - title: magentoProduct.name, - description: magentoProduct.custom_attributes.find( - (attr) => attr.attribute_code === "description" - )?.value, - status: magentoProduct.status === 1 ? "published" : "draft", - handle: magentoProduct.custom_attributes.find( - (attr) => attr.attribute_code === "url_key" - )?.value, - external_id: magentoProduct.id.toString(), - thumbnail: magentoProduct.media_gallery_entries.find( - (entry) => entry.types.includes("thumbnail") - )?.file, - sales_channels: [{ - id: data.stores[0].default_sales_channel_id, - }], - shipping_profile_id: data.shippingProfiles[0].id, - } - const existingProduct = data.existingProducts.find((p) => p.external_id === productData.external_id) - - if (existingProduct) { - productData.id = existingProduct.id - } - - productData.options = magentoProduct.extension_attributes.configurable_product_options?.map((option) => { - const attribute = data.attributes.find((attr) => attr.attribute_id === parseInt(option.attribute_id)) - return { - title: option.label, - values: attribute?.options.filter((opt) => { - return option.values.find((v) => v.value_index === parseInt(opt.value)) - }).map((opt) => opt.label) || [], - } - }) || [] - - productData.variants = magentoProduct.children?.map((child) => { - const childOptions: Record = {} - - child.custom_attributes.forEach((attr) => { - const attrData = data.attributes.find((a) => a.attribute_code === attr.attribute_code) - if (!attrData) { - return - } - - childOptions[attrData.default_frontend_label] = attrData.options.find((opt) => opt.value === attr.value)?.label || "" - }) - - const variantExternalId = child.id.toString() - const existingVariant = existingProduct.variants.find((v) => v.metadata.external_id === variantExternalId) - - return { - title: child.name, - sku: child.sku, - options: childOptions, - prices: data.stores[0].supported_currencies.map(({ currency_code }) => { - return { - amount: child.price, - currency_code, - } - }), - metadata: { - external_id: variantExternalId, - }, - id: existingVariant?.id, - } - }) - - productData.images = magentoProduct.media_gallery_entries.filter((entry) => !entry.types.includes("thumbnail")).map((entry) => { - return { - url: entry.file, - metadata: { - external_id: entry.id.toString(), - }, - } - }) - - if (productData.id) { - productsToUpdate.set(existingProduct.id, productData) - } else { - productsToCreate.set(productData.external_id!, productData) - } - }) - - return { - productsToCreate: Array.from(productsToCreate.values()), - productsToUpdate: Array.from(productsToUpdate.values()), - } -}) - -// TODO create and update products -``` - -You use `transform` again to prepare the data to create and update the products in the Medusa application. For each Magento product, you map its equivalent Medusa product's data: - -- You set the product's general details, such as the title, description, status, handle, external ID, and thumbnail using the Magento product's data and custom attributes. -- You associate the product with the default sales channel and shipping profile retrieved previously. -- You map the Magento product's configurable product options to Medusa product options. In Medusa, a product's option has a label, such as "Color", and values, such as "Red". To map the option values, you use the attributes retrieved from Magento. -- You map the Magento product's children to Medusa product variants. For the variant options, you pass an object whose keys is the option's label, such as "Color", and values is the option's value, such as "Red". For the prices, you set the variant's price based on the Magento child's price for every supported currency in the Medusa store. Also, you set the Magento child product's ID in the Medusa variant's `metadata.external_id` property. -- You map the Magento product's media gallery entries to Medusa product images. You filter out the thumbnail image and set the URL and the Magento image's ID in the Medusa image's `metadata.external_id` property. - -In addition, you use the existing products retrieved in the previous step to determine whether a product should be created or updated. If there's an existing product whose `external_id` matches the ID of the magento product, you set the existing product's ID in the `id` property of the product to be updated. You also do the same for its variants. - -Finally, you return the products to create and update. - -The last steps of the workflow is to create and update the products. Replace the `TODO` in the workflow function with the following: - -```ts title="src/workflows/migrate-products-from-magento.ts" -createProductsWorkflow.runAsStep({ - input: { - products: productsToCreate, - }, -}) - -updateProductsWorkflow.runAsStep({ - input: { - products: productsToUpdate, - }, -}) - -return new WorkflowResponse(pagination) -``` - -You use the `createProductsWorkflow` and `updateProductsWorkflow` workflows from Medusa's `@medusajs/medusa/core-flows` package to create and update the products in the Medusa application. - -Workflows must return an instance of `WorkflowResponse`, passing as a parameter the data to return to the workflow's executor. This workflow returns the pagination parameters, allowing you to paginate the product migration process. - -You can now use this workflow to migrate products from Magento to Medusa. You'll learn how to use it in the next steps. - -*** - -## Step 6: Schedule Product Migration - -There are many ways to execute tasks asynchronously in Medusa, such as [scheduling a job](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md) or [handling emitted events](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md). - -In this guide, you'll learn how to schedule the product migration at a specified interval using a scheduled job. A scheduled job is an asynchronous function that the Medusa application runs at the interval you specify during the Medusa application's runtime. - -Refer to the [Scheduled Jobs](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md) documentation to learn more about scheduled jobs. - -To create a scheduled job, in your plugin, create the file `src/jobs/migrate-magento.ts` with the following content: - -![Diagram showcasing the migrate-magento file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739358924/Medusa%20Resources/magento-7_rqoodo.jpg) - -```ts title="src/jobs/migrate-magento.ts" -import { MedusaContainer } from "@medusajs/framework/types" -import { migrateProductsFromMagentoWorkflow } from "../workflows" - -export default async function migrateMagentoJob( - container: MedusaContainer -) { - const logger = container.resolve("logger") - logger.info("Migrating products from Magento...") - - let currentPage = 0 - const pageSize = 100 - let totalCount = 0 - - do { - currentPage++ - - const { - result: pagination, - } = await migrateProductsFromMagentoWorkflow(container).run({ - input: { - currentPage, - pageSize, - }, - }) - - totalCount = pagination.total_count - } while (currentPage * pageSize < totalCount) - - logger.info("Finished migrating products from Magento") -} - -export const config = { - name: "migrate-magento-job", - schedule: "0 0 * * *", -} -``` - -A scheduled job file must export: - -- An asynchronous function that executes the job's logic. The function receives the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) as a parameter. -- An object with the job's configuration, including the name and the schedule. The schedule is a cron job pattern as a string. - -In the job function, you resolve the [logger](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md) from the container to log messages. Then, you paginate the product migration process by running the `migrateProductsFromMagentoWorkflow` workflow at each page until you've migrated all products. You use the pagination result returned by the workflow to determine whether there are more products to migrate. - -Based on the job's configurations, the Medusa application will run the job at midnight every day. - -### Test it Out - -To test out this scheduled job, first, change the configuration to run the job every minute: - -```ts title="src/jobs/migrate-magento.ts" -export const config = { - // ... - schedule: "* * * * *", -} -``` - -Then, make sure to run the `plugin:develop` command in the plugin if you haven't already: - -```bash -npx medusa plugin:develop -``` - -This ensures that the plugin's latest changes are reflected in the Medusa application. - -Finally, start the Medusa application that the plugin is installed in: - -```bash npm2yarn -npm run dev -``` - -After a minute, you'll see a message in the terminal indicating that the migration started: - -```plain title="Terminal" -info: Migrating products from Magento... -``` - -Once the migration is done, you'll see the following message: - -```plain title="Terminal" -info: Finished migrating products from Magento -``` - -To confirm that the products were migrated, open the Medusa Admin dashboard at `http://localhost:9000/app` and log in. Then, click on Products in the sidebar. You'll see your magento products in the list of products. - -![Click on products at the sidebar on the right, then view the products in the table in the middle.](https://res.cloudinary.com/dza7lstvk/image/upload/v1739359394/Medusa%20Resources/Screenshot_2025-02-12_at_1.22.44_PM_uva98i.png) - -*** - -## Next Steps - -You've now implemented the logic to migrate products from Magento to Medusa. You can re-use the plugin across Medusa applications. You can also expand on the plugin to: - -- Migrate other entities, such as orders, customers, and categories. Migrating other entities follows the same pattern as migrating products, using workflows and scheduled jobs. You only need to format the data to be migrated as needed. -- Allow triggering migrations from the Medusa Admin dashboard using [Admin Customizations](https://docs.medusajs.com/docs/learn/fundamentals/admin/index.html.md). This feature is available in the [Example Repository](https://github.com/medusajs/example-repository/tree/main/src/admin). - -If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth learning of all the concepts you've used in this guide and more. - -To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md). - - # Integrate Medusa with Resend (Email Notifications) In this guide, you'll learn how to integrate Medusa with Resend. @@ -62651,326 +62716,327 @@ 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) -- [addItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addItems/index.html.md) -- [addInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addInboundShipping/index.html.md) -- [addOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addOutboundShipping/index.html.md) -- [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) -- [deleteInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.deleteInboundShipping/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) -- [deleteOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.deleteOutboundShipping/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.list/index.html.md) -- [removeInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeInboundItem/index.html.md) -- [request](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.request/index.html.md) -- [removeOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeOutboundItem/index.html.md) -- [removeItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeItem/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.retrieve/index.html.md) -- [updateItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateItem/index.html.md) -- [updateInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateInboundShipping/index.html.md) -- [updateInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateInboundItem/index.html.md) -- [updateOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateOutboundItem/index.html.md) -- [updateOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateOutboundShipping/index.html.md) -- [batchPromotions](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.batchPromotions/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.create/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.list/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.delete/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.retrieve/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.update/index.html.md) - [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) -- [revoke](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.revoke/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) - [update](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.update/index.html.md) -- [clearToken](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.clearToken/index.html.md) +- [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) -- [fetch](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.fetch/index.html.md) - [fetchStream](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.fetchStream/index.html.md) -- [getJwtHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getJwtHeader_/index.html.md) -- [getApiKeyHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getApiKeyHeader_/index.html.md) -- [getPublishableKeyHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getPublishableKeyHeader_/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) +- [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) - [getToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getToken_/index.html.md) -- [setToken](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.setToken/index.html.md) - [initClient](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.initClient/index.html.md) +- [setToken](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.setToken/index.html.md) - [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) -- [removeItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.removeItem/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) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Currency/methods/js_sdk.admin.Currency.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Currency/methods/js_sdk.admin.Currency.retrieve/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.delete/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.create/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) +- [create](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.create/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) -- [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) -- [retrieveAddress](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.retrieveAddress/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.update/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) -- [addPromotions](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.addPromotions/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) +- [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) +- [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) - [confirmEdit](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.confirmEdit/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.list/index.html.md) -- [removePromotions](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.removePromotions/index.html.md) - [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) -- [removeShippingMethod](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.removeShippingMethod/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) +- [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) - [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) -- [addInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addInboundShipping/index.html.md) -- [addInboundItems](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addInboundItems/index.html.md) -- [addOutboundItems](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addOutboundItems/index.html.md) +- [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) +- [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) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.create/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) - [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) -- [deleteOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.deleteOutboundShipping/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) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.retrieve/index.html.md) -- [updateInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateInboundItem/index.html.md) - [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) +- [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) - [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) -- [updateOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateOutboundItem/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.delete/index.html.md) -- [deleteServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.deleteServiceZone/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) -- [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) -- [batchInventoryItemLocationLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.batchInventoryItemLocationLevels/index.html.md) -- [batchInventoryItemsLocationLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.batchInventoryItemsLocationLevels/index.html.md) -- [batchUpdateLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.batchUpdateLevels/index.html.md) -- [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) -- [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) -- [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) -- [updateLevel](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.updateLevel/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.create/index.html.md) -- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.cancel/index.html.md) -- [createShipment](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.createShipment/index.html.md) -- [accept](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.accept/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.list/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.delete/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.create/index.html.md) +- [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/Notification/methods/js_sdk.admin.Notification.list/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) +- [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) +- [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) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.retrieve/index.html.md) +- [updateLevel](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.updateLevel/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Notification/methods/js_sdk.admin.Notification.retrieve/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) +- [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) -- [requestTransfer](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.requestTransfer/index.html.md) -- [listLineItems](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.listLineItems/index.html.md) - [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) -- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.cancelRequest/index.html.md) -- [confirm](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.confirm/index.html.md) -- [addItems](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.addItems/index.html.md) -- [initiateRequest](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.initiateRequest/index.html.md) -- [updateAddedItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.updateAddedItem/index.html.md) -- [removeAddedItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.removeAddedItem/index.html.md) -- [updateOriginalItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.updateOriginalItem/index.html.md) -- [request](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.request/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.list/index.html.md) -- [capture](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.capture/index.html.md) -- [listPaymentProviders](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.listPaymentProviders/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.retrieve/index.html.md) -- [refund](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.refund/index.html.md) - [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) +- [addItems](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.addItems/index.html.md) +- [confirm](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.confirm/index.html.md) +- [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) -- [create](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.create/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.list/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.delete/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.retrieve/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.update/index.html.md) -- [batch](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batch/index.html.md) -- [batchVariantInventoryItems](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batchVariantInventoryItems/index.html.md) -- [batchVariants](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batchVariants/index.html.md) -- [confirmImport](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.confirmImport/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) -- [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) -- [deleteVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.deleteVariant/index.html.md) -- [export](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.export/index.html.md) -- [import](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.import/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.list/index.html.md) -- [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) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieve/index.html.md) -- [retrieveOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieveOption/index.html.md) -- [retrieveVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieveVariant/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.update/index.html.md) -- [updateOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.updateOption/index.html.md) -- [updateVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.updateVariant/index.html.md) +- [updateOriginalItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.updateOriginalItem/index.html.md) - [batchPrices](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.batchPrices/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.delete/index.html.md) -- [linkProducts](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.linkProducts/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.list/index.html.md) - [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) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.retrieve/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.update/index.html.md) +- [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) -- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.update/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.delete/index.html.md) - [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.updateProducts/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductVariant/methods/js_sdk.admin.ProductVariant.list/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.create/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.list/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.delete/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.retrieve/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.update/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) -- [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) +- [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) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.retrieve/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.create/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.create/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.delete/index.html.md) +- [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) +- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.create/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.retrieve/index.html.md) - [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) +- [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) - [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) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.list/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) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.list/index.html.md) - [listRuleValues](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRuleValues/index.html.md) -- [removeRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.removeRules/index.html.md) +- [listRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRules/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.retrieve/index.html.md) -- [updateRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.updateRules/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) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.delete/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.retrieve/index.html.md) +- [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) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.create/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.delete/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.retrieve/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.update/index.html.md) -- [addReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.addReturnShipping/index.html.md) -- [addReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.addReturnItem/index.html.md) -- [cancelReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancelReceive/index.html.md) -- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancel/index.html.md) -- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancelRequest/index.html.md) -- [confirmReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.confirmReceive/index.html.md) -- [deleteReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.deleteReturnShipping/index.html.md) -- [confirmRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.confirmRequest/index.html.md) -- [dismissItems](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.dismissItems/index.html.md) -- [initiateReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.initiateReceive/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.list/index.html.md) -- [initiateRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.initiateRequest/index.html.md) -- [removeDismissItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeDismissItem/index.html.md) -- [removeReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeReturnItem/index.html.md) -- [removeReceiveItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeReceiveItem/index.html.md) -- [receiveItems](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.receiveItems/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) -- [updateDismissItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateDismissItem/index.html.md) -- [updateReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReturnItem/index.html.md) -- [updateReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReturnShipping/index.html.md) -- [updateRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateRequest/index.html.md) +- [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) -- [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) - [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) +- [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) +- [addReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.addReturnShipping/index.html.md) +- [cancelReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancelReceive/index.html.md) +- [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) +- [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) +- [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) +- [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) +- [update](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.update/index.html.md) +- [updateFulfillmentProviders](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.updateFulfillmentProviders/index.html.md) +- [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) -- [updateRules](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.updateRules/index.html.md) -- [batchProducts](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.batchProducts/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.delete/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.update/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.create/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.retrieve/index.html.md) -- [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) -- [create](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.create/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) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.retrieve/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.create/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.delete/index.html.md) -- [createFulfillmentSet](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.createFulfillmentSet/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.retrieve/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.list/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.update/index.html.md) -- [updateFulfillmentProviders](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.updateFulfillmentProviders/index.html.md) -- [updateSalesChannels](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.updateSalesChannels/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/Store/methods/js_sdk.admin.Store.list/index.html.md) +- [updateRules](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.updateRules/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/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) - [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/WorkflowExecution/methods/js_sdk.admin.WorkflowExecution.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/WorkflowExecution/methods/js_sdk.admin.WorkflowExecution.retrieve/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.create/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.retrieve/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.delete/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.delete/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.create/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.list/index.html.md) -- [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/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) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.delete/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/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) - [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) @@ -62979,12 +63045,12 @@ To learn more about the commerce features that Medusa provides, check out Medusa ## JS SDK Auth - [callback](https://docs.medusajs.com/references/js-sdk/auth/callback/index.html.md) -- [login](https://docs.medusajs.com/references/js-sdk/auth/login/index.html.md) - [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) -- [register](https://docs.medusajs.com/references/js-sdk/auth/register/index.html.md) - [resetPassword](https://docs.medusajs.com/references/js-sdk/auth/resetPassword/index.html.md) - [updateProvider](https://docs.medusajs.com/references/js-sdk/auth/updateProvider/index.html.md) +- [register](https://docs.medusajs.com/references/js-sdk/auth/register/index.html.md) ## JS SDK Store @@ -62993,10 +63059,10 @@ To learn more about the commerce features that Medusa provides, check out Medusa - [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) -- [fulfillment](https://docs.medusajs.com/references/js-sdk/store/fulfillment/index.html.md) - [order](https://docs.medusajs.com/references/js-sdk/store/order/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) @@ -63019,145 +63085,6 @@ These layout components allow you to set the layout of your [UI routes](https:// These components allow you to use common Medusa Admin components in your custom UI routes and widgets. -# Single Column Layout - Admin Components - -The Medusa Admin has pages with a single column of content. - -This doesn't include the sidebar, only the main content. - -![An example of an admin page with a single column](https://res.cloudinary.com/dza7lstvk/image/upload/v1728286605/Medusa%20Resources/single-column.png) - -To create a layout that you can use in UI routes to support one column of content, create the component `src/admin/layouts/single-column.tsx` with the following content: - -```tsx title="src/admin/layouts/single-column.tsx" -export type SingleColumnLayoutProps = { - children: React.ReactNode -} - -export const SingleColumnLayout = ({ children }: SingleColumnLayoutProps) => { - return ( -
- {children} -
- ) -} -``` - -The `SingleColumnLayout` accepts the content in the `children` props. - -*** - -## Example - -Use the `SingleColumnLayout` component in your UI routes that have a single column. For example: - -```tsx title="src/admin/routes/custom/page.tsx" highlights={[["9"]]} -import { defineRouteConfig } from "@medusajs/admin-sdk" -import { ChatBubbleLeftRight } from "@medusajs/icons" -import { Container } from "../../components/container" -import { SingleColumnLayout } from "../../layouts/single-column" -import { Header } from "../../components/header" - -const CustomPage = () => { - return ( - - -
- - - ) -} - -export const config = defineRouteConfig({ - label: "Custom", - icon: ChatBubbleLeftRight, -}) - -export default CustomPage -``` - -This UI route also uses a [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) and a [Header]() custom components. - - -# Two Column Layout - Admin Components - -The Medusa Admin has pages with two columns of content. - -This doesn't include the sidebar, only the main content. - -![An example of an admin page with two columns](https://res.cloudinary.com/dza7lstvk/image/upload/v1728286690/Medusa%20Resources/two-column_sdnkg0.png) - -To create a layout that you can use in UI routes to support two columns of content, create the component `src/admin/layouts/two-column.tsx` with the following content: - -```tsx title="src/admin/layouts/two-column.tsx" -export type TwoColumnLayoutProps = { - firstCol: React.ReactNode - secondCol: React.ReactNode -} - -export const TwoColumnLayout = ({ - firstCol, - secondCol, -}: TwoColumnLayoutProps) => { - return ( -
-
- {firstCol} -
-
- {secondCol} -
-
- ) -} -``` - -The `TwoColumnLayout` accepts two props: - -- `firstCol` indicating the content of the first column. -- `secondCol` indicating the content of the second column. - -*** - -## Example - -Use the `TwoColumnLayout` component in your UI routes that have a single column. For example: - -```tsx title="src/admin/routes/custom/page.tsx" highlights={[["9"]]} -import { defineRouteConfig } from "@medusajs/admin-sdk" -import { ChatBubbleLeftRight } from "@medusajs/icons" -import { Container } from "../../components/container" -import { Header } from "../../components/header" -import { TwoColumnLayout } from "../../layouts/two-column" - -const CustomPage = () => { - return ( - -
- - } - secondCol={ - -
- - } - /> - ) -} - -export const config = defineRouteConfig({ - label: "Custom", - icon: ChatBubbleLeftRight, -}) - -export default CustomPage -``` - -This UI route also uses [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) and [Header]() custom components. - - # Action Menu - Admin Components The Medusa Admin often provides additional actions in a dropdown shown when users click a three-dot icon. @@ -63383,6 +63310,66 @@ export default ProductWidget ``` +# 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. + + # Container - Admin Components The Medusa Admin wraps each section of a page in a container. @@ -63442,6 +63429,658 @@ 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 + +The Medusa Admin has two types of forms: + +1. Create forms, created using the [FocusModal UI component](https://docs.medusajs.com/ui/components/focus-modal/index.html.md). +2. Edit or update forms, created using the [Drawer UI component](https://docs.medusajs.com/ui/components/drawer/index.html.md). + +This guide explains how to create these two form types following the Medusa Admin's conventions. + +## Form Tooling + +The Medusa Admin uses the following tools to build the forms: + +1. [react-hook-form](https://react-hook-form.com/) to easily build forms and manage their states. +2. [Zod](https://zod.dev/) to validate the form's fields. + +Both of these libraries are available in your project, so you don't have to install them to use them. + +*** + +## Create Form + +In this section, you'll build a form component to create an item of a resource. + +### Full Component + +```tsx title="src/admin/components/create-form.tsx" +import { + FocusModal, + Heading, + Label, + Input, + Button, +} from "@medusajs/ui" +import { + useForm, + FormProvider, + Controller, +} from "react-hook-form" +import * as zod from "zod" + +const schema = zod.object({ + name: zod.string(), +}) + +export const CreateForm = () => { + const form = useForm>({ + defaultValues: { + name: "", + }, + }) + + const handleSubmit = form.handleSubmit(({ name }) => { + // TODO submit to backend + console.log(name) + }) + + return ( + + + + + + +
+ +
+ + + + +
+
+ +
+
+
+ + Create Item + +
+
+ { + return ( +
+
+ +
+ +
+ ) + }} + /> +
+
+
+
+
+
+
+
+ ) +} +``` + +Unlike other components in this documentation, this form component isn't reusable. You have to create one for every resource that has a create form in the admin. + +Start by creating the file `src/admin/components/create-form.tsx` that you'll create the form in. + +### Create Validation Schema + +In `src/admin/components/create-form.tsx`, create a validation schema with Zod for the form's fields: + +```tsx title="src/admin/components/create-form.tsx" +import * as zod from "zod" + +const schema = zod.object({ + name: zod.string(), +}) +``` + +The form in this guide is simple, it only has a required `name` field, which is a string. + +### Initialize Form + +Next, you'll initialize the form using `react-hook-form`. + +Add to `src/admin/components/create-form.tsx` the following: + +```tsx title="src/admin/components/create-form.tsx" +// other imports... +import { useForm } from "react-hook-form" + +// validation schema... + +export const CreateForm = () => { + const form = useForm>({ + defaultValues: { + name: "", + }, + }) + + const handleSubmit = form.handleSubmit(({ name }) => { + // TODO submit to backend + console.log(name) + }) + + // TODO render form +} +``` + +You create the `CreateForm` component. For now, it uses `useForm` from `react-hook-form` to initialize a form. + +You also define a `handleSubmit` function to perform an action when the form is submitted. + +You can replace the content of the function with sending a request to Medusa's routes. Refer to [this guide](https://docs.medusajs.com/docs/learn/fundamentals/admin/tips#send-requests-to-api-routes/index.html.md) for more details on how to do that. + +### Render Components + +You'll now add a `return` statement that renders the focus modal where the form is shown. + +Replace `// TODO render form` with the following: + +```tsx title="src/admin/components/create-form.tsx" +// other imports... +import { + FocusModal, + Heading, + Label, + Input, + Button, +} from "@medusajs/ui" +import { + FormProvider, + Controller, +} from "react-hook-form" + +export const CreateForm = () => { + // ... + + return ( + + + + + + +
+ +
+ + + + +
+
+ +
+
+
+ + Create Item + +
+
+ { + return ( +
+
+ +
+ +
+ ) + }} + /> +
+
+
+
+
+
+
+
+ ) +} +``` + +You render a focus modal, with a trigger button to open it. + +In the `FocusModal.Content` component, you wrap the content with the `FormProvider` component from `react-hook-form`, passing it the details of the form you initialized earlier as props. + +In the `FormProvider`, you add a `form` component passing it the `handleSubmit` function you created earlier as the handler of the `onSubmit` event. + +In the `FocusModal.Header` component, you add buttons to save or cancel the form submission. + +Finally, you render the form's components inside the `FocusModal.Body`. To render inputs, you use the `Controller` component imported from `react-hook-form`. + +### Use Create Form Component + +You can use the `CreateForm` component in your 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 { CreateForm } from "../components/create-form" +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 component 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 components. + +It will add at the top of a product's details page a new section, and in its header you'll find a Create button. If you click on it, it will open the focus modal with your form. + +*** + +## Edit Form + +In this section, you'll build a form component to edit an item of a resource. + +### Full Component + +```tsx title="src/admin/components/edit-form.tsx" +import { + Drawer, + Heading, + Label, + Input, + Button, +} from "@medusajs/ui" +import { + useForm, + FormProvider, + Controller, +} from "react-hook-form" +import * as zod from "zod" + +const schema = zod.object({ + name: zod.string(), +}) + +export const EditForm = () => { + const form = useForm>({ + defaultValues: { + name: "", + }, + }) + + const handleSubmit = form.handleSubmit(({ name }) => { + // TODO submit to backend + console.log(name) + }) + + return ( + + + + + + +
+ + + Edit Item + + + + { + return ( +
+
+ +
+ +
+ ) + }} + /> +
+ +
+ + + + +
+
+
+
+
+
+ ) +} +``` + +Unlike other components in this documentation, this form component isn't reusable. You have to create one for every resource that has an edit form in the admin. + +Start by creating the file `src/admin/components/edit-form.tsx` that you'll create the form in. + +### Create Validation Schema + +In `src/admin/components/edit-form.tsx`, create a validation schema with Zod for the form's fields: + +```tsx title="src/admin/components/edit-form.tsx" +import * as zod from "zod" + +const schema = zod.object({ + name: zod.string(), +}) +``` + +The form in this guide is simple, it only has a required `name` field, which is a string. + +### Initialize Form + +Next, you'll initialize the form using `react-hook-form`. + +Add to `src/admin/components/edit-form.tsx` the following: + +```tsx title="src/admin/components/edit-form.tsx" +// other imports... +import { useForm } from "react-hook-form" + +// validation schema... + +export const EditForm = () => { + const form = useForm>({ + defaultValues: { + name: "", + }, + }) + + const handleSubmit = form.handleSubmit(({ name }) => { + // TODO submit to backend + console.log(name) + }) + + // TODO render form +} +``` + +You create the `EditForm` component. For now, it uses `useForm` from `react-hook-form` to initialize a form. + +You also define a `handleSubmit` function to perform an action when the form is submitted. + +You can replace the content of the function with sending a request to Medusa's routes. Refer to [this guide](https://docs.medusajs.com/docs/learn/fundamentals/admin/tips#send-requests-to-api-routes/index.html.md) for more details on how to do that. + +### Render Components + +You'll now add a `return` statement that renders the drawer where the form is shown. + +Replace `// TODO render form` with the following: + +```tsx title="src/admin/components/edit-form.tsx" +// other imports... +import { + Drawer, + Heading, + Label, + Input, + Button, +} from "@medusajs/ui" +import { + FormProvider, + Controller, +} from "react-hook-form" + +export const EditForm = () => { + // ... + + return ( + + + + + + +
+ + + Edit Item + + + + { + return ( +
+
+ +
+ +
+ ) + }} + /> +
+ +
+ + + + +
+
+
+
+
+
+ ) +} +``` + +You render a drawer, with a trigger button to open it. + +In the `Drawer.Content` component, you wrap the content with the `FormProvider` component from `react-hook-form`, passing it the details of the form you initialized earlier as props. + +In the `FormProvider`, you add a `form` component passing it the `handleSubmit` function you created earlier as the handler of the `onSubmit` event. + +You render the form's components inside the `Drawer.Body`. To render inputs, you use the `Controller` component imported from `react-hook-form`. + +Finally, in the `Drawer.Footer` component, you add buttons to save or cancel the form submission. + +### Use Edit Form Component + +You can use the `EditForm` component in your 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 { EditForm } from "../components/edit-form" + +const ProductWidget = () => { + return ( + +
, + }, + ]} + /> + + ) +} + +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` + +This component 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 components. + +It will add at the top of a product's details page a new section, and in its header you'll find an "Edit Item" button. If you click on it, it will open the drawer with your form. + + # Data Table - Admin Components This component is available after [Medusa v2.4.0+](https://github.com/medusajs/medusa/releases/tag/v2.4.0). @@ -64075,579 +64714,6 @@ 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. -# Forms - Admin Components - -The Medusa Admin has two types of forms: - -1. Create forms, created using the [FocusModal UI component](https://docs.medusajs.com/ui/components/focus-modal/index.html.md). -2. Edit or update forms, created using the [Drawer UI component](https://docs.medusajs.com/ui/components/drawer/index.html.md). - -This guide explains how to create these two form types following the Medusa Admin's conventions. - -## Form Tooling - -The Medusa Admin uses the following tools to build the forms: - -1. [react-hook-form](https://react-hook-form.com/) to easily build forms and manage their states. -2. [Zod](https://zod.dev/) to validate the form's fields. - -Both of these libraries are available in your project, so you don't have to install them to use them. - -*** - -## Create Form - -In this section, you'll build a form component to create an item of a resource. - -### Full Component - -```tsx title="src/admin/components/create-form.tsx" -import { - FocusModal, - Heading, - Label, - Input, - Button, -} from "@medusajs/ui" -import { - useForm, - FormProvider, - Controller, -} from "react-hook-form" -import * as zod from "zod" - -const schema = zod.object({ - name: zod.string(), -}) - -export const CreateForm = () => { - const form = useForm>({ - defaultValues: { - name: "", - }, - }) - - const handleSubmit = form.handleSubmit(({ name }) => { - // TODO submit to backend - console.log(name) - }) - - return ( - - - - - - -
- -
- - - - -
-
- -
-
-
- - Create Item - -
-
- { - return ( -
-
- -
- -
- ) - }} - /> -
-
-
-
-
-
-
-
- ) -} -``` - -Unlike other components in this documentation, this form component isn't reusable. You have to create one for every resource that has a create form in the admin. - -Start by creating the file `src/admin/components/create-form.tsx` that you'll create the form in. - -### Create Validation Schema - -In `src/admin/components/create-form.tsx`, create a validation schema with Zod for the form's fields: - -```tsx title="src/admin/components/create-form.tsx" -import * as zod from "zod" - -const schema = zod.object({ - name: zod.string(), -}) -``` - -The form in this guide is simple, it only has a required `name` field, which is a string. - -### Initialize Form - -Next, you'll initialize the form using `react-hook-form`. - -Add to `src/admin/components/create-form.tsx` the following: - -```tsx title="src/admin/components/create-form.tsx" -// other imports... -import { useForm } from "react-hook-form" - -// validation schema... - -export const CreateForm = () => { - const form = useForm>({ - defaultValues: { - name: "", - }, - }) - - const handleSubmit = form.handleSubmit(({ name }) => { - // TODO submit to backend - console.log(name) - }) - - // TODO render form -} -``` - -You create the `CreateForm` component. For now, it uses `useForm` from `react-hook-form` to initialize a form. - -You also define a `handleSubmit` function to perform an action when the form is submitted. - -You can replace the content of the function with sending a request to Medusa's routes. Refer to [this guide](https://docs.medusajs.com/docs/learn/fundamentals/admin/tips#send-requests-to-api-routes/index.html.md) for more details on how to do that. - -### Render Components - -You'll now add a `return` statement that renders the focus modal where the form is shown. - -Replace `// TODO render form` with the following: - -```tsx title="src/admin/components/create-form.tsx" -// other imports... -import { - FocusModal, - Heading, - Label, - Input, - Button, -} from "@medusajs/ui" -import { - FormProvider, - Controller, -} from "react-hook-form" - -export const CreateForm = () => { - // ... - - return ( - - - - - - -
- -
- - - - -
-
- -
-
-
- - Create Item - -
-
- { - return ( -
-
- -
- -
- ) - }} - /> -
-
-
-
-
-
-
-
- ) -} -``` - -You render a focus modal, with a trigger button to open it. - -In the `FocusModal.Content` component, you wrap the content with the `FormProvider` component from `react-hook-form`, passing it the details of the form you initialized earlier as props. - -In the `FormProvider`, you add a `form` component passing it the `handleSubmit` function you created earlier as the handler of the `onSubmit` event. - -In the `FocusModal.Header` component, you add buttons to save or cancel the form submission. - -Finally, you render the form's components inside the `FocusModal.Body`. To render inputs, you use the `Controller` component imported from `react-hook-form`. - -### Use Create Form Component - -You can use the `CreateForm` component in your 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 { CreateForm } from "../components/create-form" -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 component 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 components. - -It will add at the top of a product's details page a new section, and in its header you'll find a Create button. If you click on it, it will open the focus modal with your form. - -*** - -## Edit Form - -In this section, you'll build a form component to edit an item of a resource. - -### Full Component - -```tsx title="src/admin/components/edit-form.tsx" -import { - Drawer, - Heading, - Label, - Input, - Button, -} from "@medusajs/ui" -import { - useForm, - FormProvider, - Controller, -} from "react-hook-form" -import * as zod from "zod" - -const schema = zod.object({ - name: zod.string(), -}) - -export const EditForm = () => { - const form = useForm>({ - defaultValues: { - name: "", - }, - }) - - const handleSubmit = form.handleSubmit(({ name }) => { - // TODO submit to backend - console.log(name) - }) - - return ( - - - - - - -
- - - Edit Item - - - - { - return ( -
-
- -
- -
- ) - }} - /> -
- -
- - - - -
-
-
-
-
-
- ) -} -``` - -Unlike other components in this documentation, this form component isn't reusable. You have to create one for every resource that has an edit form in the admin. - -Start by creating the file `src/admin/components/edit-form.tsx` that you'll create the form in. - -### Create Validation Schema - -In `src/admin/components/edit-form.tsx`, create a validation schema with Zod for the form's fields: - -```tsx title="src/admin/components/edit-form.tsx" -import * as zod from "zod" - -const schema = zod.object({ - name: zod.string(), -}) -``` - -The form in this guide is simple, it only has a required `name` field, which is a string. - -### Initialize Form - -Next, you'll initialize the form using `react-hook-form`. - -Add to `src/admin/components/edit-form.tsx` the following: - -```tsx title="src/admin/components/edit-form.tsx" -// other imports... -import { useForm } from "react-hook-form" - -// validation schema... - -export const EditForm = () => { - const form = useForm>({ - defaultValues: { - name: "", - }, - }) - - const handleSubmit = form.handleSubmit(({ name }) => { - // TODO submit to backend - console.log(name) - }) - - // TODO render form -} -``` - -You create the `EditForm` component. For now, it uses `useForm` from `react-hook-form` to initialize a form. - -You also define a `handleSubmit` function to perform an action when the form is submitted. - -You can replace the content of the function with sending a request to Medusa's routes. Refer to [this guide](https://docs.medusajs.com/docs/learn/fundamentals/admin/tips#send-requests-to-api-routes/index.html.md) for more details on how to do that. - -### Render Components - -You'll now add a `return` statement that renders the drawer where the form is shown. - -Replace `// TODO render form` with the following: - -```tsx title="src/admin/components/edit-form.tsx" -// other imports... -import { - Drawer, - Heading, - Label, - Input, - Button, -} from "@medusajs/ui" -import { - FormProvider, - Controller, -} from "react-hook-form" - -export const EditForm = () => { - // ... - - return ( - - - - - - -
- - - Edit Item - - - - { - return ( -
-
- -
- -
- ) - }} - /> -
- -
- - - - -
-
-
-
-
-
- ) -} -``` - -You render a drawer, with a trigger button to open it. - -In the `Drawer.Content` component, you wrap the content with the `FormProvider` component from `react-hook-form`, passing it the details of the form you initialized earlier as props. - -In the `FormProvider`, you add a `form` component passing it the `handleSubmit` function you created earlier as the handler of the `onSubmit` event. - -You render the form's components inside the `Drawer.Body`. To render inputs, you use the `Controller` component imported from `react-hook-form`. - -Finally, in the `Drawer.Footer` component, you add buttons to save or cancel the form submission. - -### Use Edit Form Component - -You can use the `EditForm` component in your 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 { EditForm } from "../components/edit-form" - -const ProductWidget = () => { - return ( - -
, - }, - ]} - /> - - ) -} - -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) - -export default ProductWidget -``` - -This component 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 components. - -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. - - # JSON View - Admin Components Detail pages in the Medusa Admin show a JSON section to view the current page's details in JSON format. @@ -65323,92 +65389,163 @@ const posts = await postModuleService.createPosts([ If an array is passed of the method, an array of the created records is also returned. -# restore Method - Service Factory Reference +# delete 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). +This method deletes one or more records. -## Restore One Record +## Delete One Record ```ts -const restoredPosts = await postModuleService.restorePosts("123") +await postModuleService.deletePosts("123") ``` -### Parameters - -To restore one record, pass its ID as a parameter of the method. - -### Returns - -The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of restored records' IDs. - -For example, the returned object of the above example is: - -```ts -restoredPosts = { - post_id: ["123"], -} -``` +To delete one record, pass its ID as a parameter of the method. *** -## Restore Multiple Records +## Delete Multiple Records ```ts -const restoredPosts = await postModuleService.restorePosts([ +await postModuleService.deletePosts([ "123", "321", ]) ``` -### Parameters - -To restore multiple records, pass an array of IDs as a parameter of the method. - -### Returns - -The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of restored records' IDs. - -For example, the returned object of the above example is: - -```ts -restoredPosts = { - post_id: [ - "123", - "321", - ], -} -``` +To delete multiple records, pass an array of IDs as a parameter of the method. *** -## Restore Records Matching Filters +## Delete Records Matching Filters ```ts -const restoredPosts = await postModuleService.restorePosts({ +await postModuleService.deletePosts({ name: "My Post", }) ``` +To delete records matching a set of filters, pass an object of filters as a parameter. + +Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md). + + +# list Method - Service Factory Reference + +This method retrieves a list of records. + +## Retrieve List of Records + +```ts +const posts = await postModuleService.listPosts() +``` + +If no parameters are passed, the method returns an array of the first `15` records. + +*** + +## Filter Records + +```ts +const posts = await postModuleService.listPosts({ + id: ["123", "321"], +}) +``` + ### Parameters -To restore records matching a set of filters, pass an object of fitlers as a parameter of the method. +To retrieve records matching a set of filters, pass an object of the filters as a first parameter. Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md). ### Returns -The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of restored records' IDs. +The method returns an array of the first `15` records matching the filters. -For example, the returned object of the above example is: +*** + +## Retrieve Relations + +This applies to relations between data models of the same module. To retrieve linked records of different modules, use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md). ```ts -restoredPosts = { - post_id: [ - "123", - ], -} +const posts = await postModuleService.listPosts({}, { + relations: ["author"], +}) ``` +### Parameters + +To retrieve records with their relations, pass as a second parameter an object having a `relations` property. `relations`'s value is an array of relation names. + +### Returns + +The method returns an array of the first `15` records matching the filters. + +*** + +## Select Properties + +```ts +const posts = await postModuleService.listPosts({}, { + select: ["id", "name"], +}) +``` + +### Parameters + +By default, retrieved records have all their properties. To select specific properties to retrieve, pass in the second object parameter a `select` property. + +`select`'s value is an array of property names to retrieve. + +### Returns + +The method returns an array of the first `15` records matching the filters. + +*** + +## Paginate Relations + +```ts +const posts = await postModuleService.listPosts({}, { + take: 20, + skip: 10, +}) +``` + +### Parameters + +To paginate the returned records, the second object parameter accepts the following properties: + +- `take`: a number indicating how many records to retrieve. By default, it's `15`. +- `skip`: a number indicating how many records to skip before the retrieved records. By default, it's `0`. + +### Returns + +The method returns an array of records. The number of records is less than or equal to `take`'s value. + +*** + +## Sort Records + +```ts +const posts = await postModuleService.listPosts({}, { + order: { + name: "ASC", + }, +}) +``` + +### Parameters + +To sort records by one or more properties, pass to the second object parameter the `order` property. Its value is an object whose keys are the property names, and values can either be: + +- `ASC` to sort by this property in the ascending order. +- `DESC` to sort by this property in the descending order. + +### Returns + +The method returns an array of the first `15` records matching the filters. + # listAndCount Method - Service Factory Reference @@ -65546,123 +65683,92 @@ The method returns an array with two items: 2. The second is the total count of records. -# list Method - Service Factory Reference +# restore Method - Service Factory Reference -This method retrieves a list of records. +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). -## Retrieve List of Records +## Restore One Record ```ts -const posts = await postModuleService.listPosts() +const restoredPosts = await postModuleService.restorePosts("123") ``` -If no parameters are passed, the method returns an array of the first `15` records. +### Parameters + +To restore one record, pass its ID as a parameter of the method. + +### Returns + +The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of restored records' IDs. + +For example, the returned object of the above example is: + +```ts +restoredPosts = { + post_id: ["123"], +} +``` *** -## Filter Records +## Restore Multiple Records ```ts -const posts = await postModuleService.listPosts({ - id: ["123", "321"], +const restoredPosts = await postModuleService.restorePosts([ + "123", + "321", +]) +``` + +### Parameters + +To restore multiple records, pass an array of IDs as a parameter of the method. + +### Returns + +The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of restored records' IDs. + +For example, the returned object of the above example is: + +```ts +restoredPosts = { + post_id: [ + "123", + "321", + ], +} +``` + +*** + +## Restore Records Matching Filters + +```ts +const restoredPosts = await postModuleService.restorePosts({ + name: "My Post", }) ``` ### Parameters -To retrieve records matching a set of filters, pass an object of the filters as a first parameter. +To restore records matching a set of filters, pass an object of fitlers as a parameter of the method. Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md). ### Returns -The method returns an array of the first `15` records matching the filters. +The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of restored records' IDs. -*** - -## Retrieve Relations - -This applies to relations between data models of the same module. To retrieve linked records of different modules, use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md). +For example, the returned object of the above example is: ```ts -const posts = await postModuleService.listPosts({}, { - relations: ["author"], -}) +restoredPosts = { + post_id: [ + "123", + ], +} ``` -### Parameters - -To retrieve records with their relations, pass as a second parameter an object having a `relations` property. `relations`'s value is an array of relation names. - -### Returns - -The method returns an array of the first `15` records matching the filters. - -*** - -## Select Properties - -```ts -const posts = await postModuleService.listPosts({}, { - select: ["id", "name"], -}) -``` - -### Parameters - -By default, retrieved records have all their properties. To select specific properties to retrieve, pass in the second object parameter a `select` property. - -`select`'s value is an array of property names to retrieve. - -### Returns - -The method returns an array of the first `15` records matching the filters. - -*** - -## Paginate Relations - -```ts -const posts = await postModuleService.listPosts({}, { - take: 20, - skip: 10, -}) -``` - -### Parameters - -To paginate the returned records, the second object parameter accepts the following properties: - -- `take`: a number indicating how many records to retrieve. By default, it's `15`. -- `skip`: a number indicating how many records to skip before the retrieved records. By default, it's `0`. - -### Returns - -The method returns an array of records. The number of records is less than or equal to `take`'s value. - -*** - -## Sort Records - -```ts -const posts = await postModuleService.listPosts({}, { - order: { - name: "ASC", - }, -}) -``` - -### Parameters - -To sort records by one or more properties, pass to the second object parameter the `order` property. Its value is an object whose keys are the property names, and values can either be: - -- `ASC` to sort by this property in the ascending order. -- `DESC` to sort by this property in the descending order. - -### Returns - -The method returns an array of the first `15` records matching the filters. - # retrieve Method - Service Factory Reference @@ -65721,46 +65827,6 @@ By default, all of the record's properties are retrieved. To select specific one The method returns the record as an object. -# delete Method - Service Factory Reference - -This method deletes one or more records. - -## Delete One Record - -```ts -await postModuleService.deletePosts("123") -``` - -To delete one record, pass its ID as a parameter of the method. - -*** - -## Delete Multiple Records - -```ts -await postModuleService.deletePosts([ - "123", - "321", -]) -``` - -To delete multiple records, pass an array of IDs as a parameter of the method. - -*** - -## Delete Records Matching Filters - -```ts -await postModuleService.deletePosts({ - name: "My Post", -}) -``` - -To delete records matching a set of filters, pass an object of filters as a parameter. - -Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md). - - # softDelete Method - Service Factory Reference This method soft deletes one or more records of the data model. @@ -65848,6 +65914,129 @@ deletedPosts = { ``` +# update Method - Service Factory Reference + +This method updates one or more records of the data model. + +## Update One Record + +```ts +const post = await postModuleService.updatePosts({ + id: "123", + name: "My Post", +}) +``` + +### Parameters + +To update one record, pass an object that at least has an `id` property, identifying the ID of the record to update. + +You can pass in the same object any other properties to update. + +### Returns + +The method returns the updated record as an object. + +*** + +## Update Multiple Records + +```ts +const posts = await postModuleService.updatePosts([ + { + id: "123", + name: "My Post", + }, + { + id: "321", + published_at: new Date(), + }, +]) +``` + +### Parameters + +To update multiple records, pass an array of objects. Each object has at least an `id` property, identifying the ID of the record to update. + +You can pass in each object any other properties to update. + +### Returns + +The method returns an array of objects of updated records. + +*** + +## Update Records Matching a Filter + +```ts +const posts = await postModuleService.updatePosts({ + selector: { + name: "My Post", + }, + data: { + published_at: new Date(), + }, +}) +``` + +### Parameters + +To update records that match specified filters, pass as a parameter an object having two properties: + +- `selector`: An object of filters that a record must match to be updated. +- `data`: An object of the properties to update in every record that match the filters in `selector`. + +In the example above, you update the `published_at` property of every post record whose name is `My Post`. + +Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md). + +### Returns + +The method returns an array of objects of updated records. + +*** + +## Multiple Record Updates with Filters + +```ts +const posts = await postModuleService.updatePosts([ + { + selector: { + name: "My Post", + }, + data: { + published_at: new Date(), + }, + }, + { + selector: { + name: "Another Post", + }, + data: { + metadata: { + external_id: "123", + }, + }, + }, +]) +``` + +### Parameters + +To update records matching different sets of filters, pass an array of objects, each having two properties: + +- `selector`: An object of filters that a record must match to be updated. +- `data`: An object of the properties to update in every record that match the filters in `selector`. + +In the example above, you update the `published_at` property of post records whose name is `My Post`, and update the `metadata` property of post records whose name is `Another Post`. + +Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md). + +### Returns + +The method returns an array of objects of updated records. + + # Filter Records - Service Factory Reference Many of the service factory's generated methods allow passing filters to perform an operation, such as to update or delete records matching the filters. @@ -66135,129 +66324,6 @@ The following operators are supported by the service factory filtering mechanism |\`$not\`|Inverts the logic of a condition. For example, | -# update Method - Service Factory Reference - -This method updates one or more records of the data model. - -## Update One Record - -```ts -const post = await postModuleService.updatePosts({ - id: "123", - name: "My Post", -}) -``` - -### Parameters - -To update one record, pass an object that at least has an `id` property, identifying the ID of the record to update. - -You can pass in the same object any other properties to update. - -### Returns - -The method returns the updated record as an object. - -*** - -## Update Multiple Records - -```ts -const posts = await postModuleService.updatePosts([ - { - id: "123", - name: "My Post", - }, - { - id: "321", - published_at: new Date(), - }, -]) -``` - -### Parameters - -To update multiple records, pass an array of objects. Each object has at least an `id` property, identifying the ID of the record to update. - -You can pass in each object any other properties to update. - -### Returns - -The method returns an array of objects of updated records. - -*** - -## Update Records Matching a Filter - -```ts -const posts = await postModuleService.updatePosts({ - selector: { - name: "My Post", - }, - data: { - published_at: new Date(), - }, -}) -``` - -### Parameters - -To update records that match specified filters, pass as a parameter an object having two properties: - -- `selector`: An object of filters that a record must match to be updated. -- `data`: An object of the properties to update in every record that match the filters in `selector`. - -In the example above, you update the `published_at` property of every post record whose name is `My Post`. - -Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md). - -### Returns - -The method returns an array of objects of updated records. - -*** - -## Multiple Record Updates with Filters - -```ts -const posts = await postModuleService.updatePosts([ - { - selector: { - name: "My Post", - }, - data: { - published_at: new Date(), - }, - }, - { - selector: { - name: "Another Post", - }, - data: { - metadata: { - external_id: "123", - }, - }, - }, -]) -``` - -### Parameters - -To update records matching different sets of filters, pass an array of objects, each having two properties: - -- `selector`: An object of filters that a record must match to be updated. -- `data`: An object of the properties to update in every record that match the filters in `selector`. - -In the example above, you update the `published_at` property of post records whose name is `My Post`, and update the `metadata` property of post records whose name is `Another Post`. - -Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md). - -### Returns - -The method returns an array of objects of updated records. - -

Just Getting Started?

@@ -66718,125 +66784,6 @@ How to install and setup Medusa UI. -# Medusa Admin Extension - -How to install and use Medusa UI for building Admin extensions. - -## Installation - -*** - -The `@medusajs/ui` package is a already installed as a dependency of the `@medusajs/admin` package. Due to this you can simply import the package and use it in your local Admin extensions. - -If you are building a Admin extension as part of a Medusa plugin, you can install the package as a dependency of your plugin. - -```bash -npm install @medusajs/ui -``` - -## Configuration - -*** - -The configuration of the UI package is handled by the `@medusajs/admin` package. Therefore, you do not need to any additional configuration to use the UI package in your Admin extensions. - - -# Standalone Project - -How to install and use Medusa UI in a standalone project. - -## Installation - -*** - -Medusa UI is a React UI library and while it's intended for usage within Medusa projects, it can also be used in any React project. - -### Install Medusa UI - -Install the React UI library with the following command: - -```bash -npm install @medusajs/ui -``` - -### Configuring Tailwind CSS - -The components are styled using Tailwind CSS, and in order to use them, you will need to install Tailwind CSS in your project as well. -For more information on how to install Tailwind CSS, please refer to the [Tailwind CSS documentation](https://tailwindcss.com/docs/installation). - -All of the classes used for Medusa UI are shipped as a Tailwind CSS customization. -You can install it with the following command: - -```bash -npm install @medusajs/ui-preset -``` - -After you have installed Tailwind CSS and the Medusa UI preset, you need to add the following to your `tailwind.config.js`file: - -```tsx -module.exports = { - presets: [require("@medusajs/ui-preset")], - // ... -} -``` - -In order for the styles to be applied correctly to the components, you will also need to ensure that -`@medusajs/ui` is included in the content field of your `tailwind.config.js` file: - -```tsx -module.exports = { - content: [ - // ... - "./node_modules/@medusajs/ui/dist/**/*.{js,jsx,ts,tsx}", - ], - // ... -} -``` - -If you are working within a monorepo, you may need to add the path to the `@medusajs/ui` package in your `tailwind.config.js` like so: - -```tsx -const path = require("path") - -const uiPath = path.resolve( - require.resolve("@medusajs/ui"), - "../..", - "\*_/_.{js,jsx,ts,tsx}" -) - -module.exports = { - content: [ - // ... - uiPath, - ], - // ... -} - -``` - -## Start building - -*** - -You are now ready to start building your application with Medusa UI. You can import the components like so: - -```tsx -import { Button, Drawer } from "@medusajs/ui" -``` - -## Updating UI Packages - -*** - -Medusa's design-system packages, including `@medusajs/ui`, `@medusajs/ui-preset`, and `@medusajs/ui-icons`, are versioned independently. However, they're still part of the latest Medusa release. So, you can browse the [release notes](https://github.com/medusajs/medusa/releases) to see if there are any breaking changes to these packages. - -To update these packages, update their version in your `package.json` file and re-install dependencies. For example: - -```bash -npm install @medusajs/ui -``` - - # Alert A component for displaying important messages. @@ -73258,6 +73205,125 @@ If you're using the `Tooltip` component in a project other than the Medusa Admin - disableHoverableContent: (boolean) When \`true\`, trying to hover the content will result in the tooltip closing as the pointer leaves the trigger. +# Medusa Admin Extension + +How to install and use Medusa UI for building Admin extensions. + +## Installation + +*** + +The `@medusajs/ui` package is a already installed as a dependency of the `@medusajs/admin` package. Due to this you can simply import the package and use it in your local Admin extensions. + +If you are building a Admin extension as part of a Medusa plugin, you can install the package as a dependency of your plugin. + +```bash +npm install @medusajs/ui +``` + +## Configuration + +*** + +The configuration of the UI package is handled by the `@medusajs/admin` package. Therefore, you do not need to any additional configuration to use the UI package in your Admin extensions. + + +# Standalone Project + +How to install and use Medusa UI in a standalone project. + +## Installation + +*** + +Medusa UI is a React UI library and while it's intended for usage within Medusa projects, it can also be used in any React project. + +### Install Medusa UI + +Install the React UI library with the following command: + +```bash +npm install @medusajs/ui +``` + +### Configuring Tailwind CSS + +The components are styled using Tailwind CSS, and in order to use them, you will need to install Tailwind CSS in your project as well. +For more information on how to install Tailwind CSS, please refer to the [Tailwind CSS documentation](https://tailwindcss.com/docs/installation). + +All of the classes used for Medusa UI are shipped as a Tailwind CSS customization. +You can install it with the following command: + +```bash +npm install @medusajs/ui-preset +``` + +After you have installed Tailwind CSS and the Medusa UI preset, you need to add the following to your `tailwind.config.js`file: + +```tsx +module.exports = { + presets: [require("@medusajs/ui-preset")], + // ... +} +``` + +In order for the styles to be applied correctly to the components, you will also need to ensure that +`@medusajs/ui` is included in the content field of your `tailwind.config.js` file: + +```tsx +module.exports = { + content: [ + // ... + "./node_modules/@medusajs/ui/dist/**/*.{js,jsx,ts,tsx}", + ], + // ... +} +``` + +If you are working within a monorepo, you may need to add the path to the `@medusajs/ui` package in your `tailwind.config.js` like so: + +```tsx +const path = require("path") + +const uiPath = path.resolve( + require.resolve("@medusajs/ui"), + "../..", + "\*_/_.{js,jsx,ts,tsx}" +) + +module.exports = { + content: [ + // ... + uiPath, + ], + // ... +} + +``` + +## Start building + +*** + +You are now ready to start building your application with Medusa UI. You can import the components like so: + +```tsx +import { Button, Drawer } from "@medusajs/ui" +``` + +## Updating UI Packages + +*** + +Medusa's design-system packages, including `@medusajs/ui`, `@medusajs/ui-preset`, and `@medusajs/ui-icons`, are versioned independently. However, they're still part of the latest Medusa release. So, you can browse the [release notes](https://github.com/medusajs/medusa/releases) to see if there are any breaking changes to these packages. + +To update these packages, update their version in your `package.json` file and re-install dependencies. For example: + +```bash +npm install @medusajs/ui +``` + + # clx Utility function for working with classNames. diff --git a/www/apps/resources/app/js-sdk/page.mdx b/www/apps/resources/app/js-sdk/page.mdx index 8049e2bfdc..09e5f1ce71 100644 --- a/www/apps/resources/app/js-sdk/page.mdx +++ b/www/apps/resources/app/js-sdk/page.mdx @@ -42,7 +42,7 @@ For admin customizations, create this file at `src/admin/lib/config.ts`. - + ```ts title="src/admin/lib/config.ts" import Medusa from "@medusajs/js-sdk" @@ -54,6 +54,21 @@ export const sdk = new Medusa({ type: "session", }, }) +``` + + + + +```ts title="src/admin/lib/config.ts" +import Medusa from "@medusajs/js-sdk" + +export const sdk = new Medusa({ + baseUrl: __BACKEND_URL__ || "/", + debug: import.meta.env.DEV, + auth: { + type: "session", + }, +}) ``` @@ -80,7 +95,7 @@ export const sdk = new Medusa({ -In the Medusa Admin you use `import.meta.env` to access environment variables becaues the Medusa Admin is built on top of [Vite](https://vitejs.dev/). Learn more in [this documentation](!docs!/learn/fundamentals/admin/environment-variables). +In Medusa Admin customizations that are created in a Medusa project, you use `import.meta.env` to access environment variables, whereas in customizations built in a Medusa plugin, you use the global variable `__BACKEND_URL__` to access the backend URL. You can learn more in the [Admin Environment Variables](!docs!/learn/fundamentals/admin/environment-variables) chapter. diff --git a/www/apps/resources/generated/edit-dates.mjs b/www/apps/resources/generated/edit-dates.mjs index 4b7332d151..8ec9df7277 100644 --- a/www/apps/resources/generated/edit-dates.mjs +++ b/www/apps/resources/generated/edit-dates.mjs @@ -2168,7 +2168,7 @@ export const generatedEditDates = { "app/commerce-modules/store/links-to-other-modules/page.mdx": "2025-04-17T16:03:16.419Z", "app/examples/page.mdx": "2025-04-17T08:50:17.036Z", "app/medusa-cli/commands/build/page.mdx": "2024-11-11T11:00:49.665Z", - "app/js-sdk/page.mdx": "2025-03-28T09:50:07.617Z", + "app/js-sdk/page.mdx": "2025-05-26T15:08:16.590Z", "references/js_sdk/admin/Admin/properties/js_sdk.admin.Admin.apiKey/page.mdx": "2025-05-20T07:51:40.924Z", "references/js_sdk/admin/Admin/properties/js_sdk.admin.Admin.campaign/page.mdx": "2025-05-20T07:51:40.925Z", "references/js_sdk/admin/Admin/properties/js_sdk.admin.Admin.claim/page.mdx": "2025-05-20T07:51:40.925Z",