diff --git a/www/apps/book/public/llms-full.txt b/www/apps/book/public/llms-full.txt index b09401e202..d7ceb45910 100644 --- a/www/apps/book/public/llms-full.txt +++ b/www/apps/book/public/llms-full.txt @@ -330,28 +330,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. @@ -458,110 +436,680 @@ npm install ``` -# Build Custom Features +# Storefront Development -In the upcoming chapters, you'll follow step-by-step guides to build custom features in Medusa. These guides gradually introduce Medusa's concepts to help you understand what they are and how to use them. +The Medusa 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. -By following these guides, you'll add brands to the Medusa application that you can associate with products. +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. -To build a custom feature in Medusa, you need three main tools: - -- [Module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md): a package with commerce logic for a single domain. It defines new tables to add to the database, and a class of methods to manage these tables. -- [Workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md): a tool to perform an operation comprising multiple steps with built-in rollback and retry mechanisms. -- [API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md): a REST endpoint that exposes commerce features to clients, such as the admin dashboard or a storefront. The API route executes a workflow that implements the commerce feature using modules. - -![Diagram showcasing the flow of a custom developed feature](https://res.cloudinary.com/dza7lstvk/image/upload/v1725867628/Medusa%20Book/custom-development_nofvp6.jpg) +- [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) *** -## Next Chapters: Brand Module Example +## Passing a Publishable API Key in Storefront Requests -The next chapters will guide you to: +When sending a request to an API route starting with `/store`, you must include a publishable API key in the header of your request. -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. +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). -# Customize Medusa Admin Dashboard +# Configure Instrumentation -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). +In this chapter, you'll learn about observability in Medusa and how to configure instrumentation with OpenTelemetry. -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: +## Observability with OpenTelemtry -- 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). +Medusa uses [OpenTelemetry](https://opentelemetry.io/) for instrumentation and reporting. When configured, it reports traces for: -From these customizations, you can send requests to custom API routes, allowing admin users to manage custom resources on the dashboard +- HTTP requests +- Workflow executions +- Query usages +- Database queries and operations *** -## Next Chapters: View Brands in Dashboard +## How to Configure Instrumentation in Medusa? -In the next chapters, you'll continue with the brands example to: +### Prerequisites -- 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. +- [An exporter to visualize your application's traces, such as Zipkin.](https://zipkin.io/pages/quickstart.html) +### Install Dependencies -# Extend Core Commerce Features +Start by installing the following OpenTelemetry dependencies in your Medusa project: -In the upcoming chapters, you'll learn about the concepts and tools to extend Medusa's core commerce features. +```bash npm2yarn +npm install @opentelemetry/sdk-node @opentelemetry/resources @opentelemetry/sdk-trace-node @opentelemetry/instrumentation-pg +``` -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. +Also, install the dependencies relevant for the exporter you use. If you're using Zipkin, install the following dependencies: -The Medusa Framework and orchestration tools mitigate these issues while supporting all your customization needs: +```bash npm2yarn +npm install @opentelemetry/exporter-zipkin +``` -- [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. +### 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. *** -## Next Chapters: Link Brands to Products Example +## Test it Out -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: +To test it out, start your exporter, such as Zipkin. -- 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. +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. -# Customizations Next Steps: Learn the Fundamentals +# Logging -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. +In this chapter, you’ll learn how to use Medusa’s logging utility. -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. +## Logger Class -## Useful Guides +Medusa provides a `Logger` class with advanced logging functionalities. This includes configuring logging levels or saving logs to a file. -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. +The Medusa application registers the `Logger` class in the Medusa container and each module's container as `logger`. *** -## More Examples in Recipes +## How to Log a Message -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. +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. -# Re-Use Customizations with Plugins +# Medusa Testing Tools -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. +In this chapter, you'll learn about Medusa's testing tools and how to install and configure them. -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. +## @medusajs/test-utils Package -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. +Medusa provides a Testing Framework to create integration tests for your custom API routes, modules, or other Medusa customizations. -![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) +To use the Testing Framework, install `@medusajs/test-utils` as a `devDependency`: -Medusa provides the tooling to create a plugin package, test it in a local Medusa application, and publish it to NPM. +```bash npm2yarn +npm install --save-dev @medusajs/test-utils@latest +``` -To learn more about plugins and how to create them, refer to [this chapter](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). +*** + +## 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. + + +# General Medusa Application Deployment Guide + +In this document, you'll learn the general steps to deploy your Medusa application. How you apply these steps depend on your chosen hosting provider or platform. + +Find how-to guides for specific platforms in [this documentation](https://docs.medusajs.com/resources/deployment/index.html.md). + +Want Medusa to manage and maintain your infrastructure? [Sign up and learn more about Medusa Cloud](https://medusajs.com/pricing) + +Medusa Cloud is our managed services offering that makes deploying and operating Medusa applications possible without having to worry about configuring, scaling, and maintaining infrastructure. Medusa Cloud hosts your server, Admin dashboard, database, and Redis instance. + +With Medusa Cloud, you maintain full customization control as you deploy your own modules and customizations directly from GitHub: + +- Push to deploy. +- Multiple testing environments. +- Preview environments for new PRs. +- Test on production-like data. + +### Prerequisites + +- [Medusa application’s codebase hosted on GitHub repository.](https://docs.medusajs.com/learn/index.html.md) + +## What You'll Deploy + +When you deploy the Medusa application, you need to deploy the following resources: + +1. PostgreSQL database: This is the database that will hold your Medusa application's details. +2. Redis database: This is the database that will store the Medusa server's session. +3. Medusa application in [server and worker mode](https://docs.medusajs.com/learn/production/worker-mode/index.html.md), where: + - The server mode handles incoming API requests and serving the Medusa Admin dashboard. + - The worker mode handles background tasks, such as scheduled jobs and subscribers. + +So, when choosing a hosting provider, make sure it supports deploying these resources. Also, for optimal experience, the hosting provider and plan must offer at least 2GB of RAM. + +*** + +## 1. Configure Medusa Application + +### Worker Mode + +The `workerMode` configuration determines which mode the Medusa application runs in. When you deploy the Medusa application, you deploy two instances: one in server mode, and one in worker mode. + +Learn more about worker mode in the [Worker Module chapter](https://docs.medusajs.com/learn/production/worker-mode/index.html.md). + +So, add the following configuration in `medusa-config.ts`: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + // ... + workerMode: process.env.MEDUSA_WORKER_MODE as "shared" | "worker" | "server", + }, +}) +``` + +Later, you’ll set different values of the `MEDUSA_WORKER_MODE` environment variable for each Medusa application deployment or instance. + +### Configure Medusa Admin + +The Medusa Admin is served by the Medusa server application. So, you need to disable it in the worker Medusa application only. + +To disable the Medusa Admin in the worker Medusa application while keeping it enabled in the server Medusa application, add the following configuration in `medusa-config.ts`: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + admin: { + disable: process.env.DISABLE_MEDUSA_ADMIN === "true", + }, +}) +``` + +Later, you’ll set different values of the `DISABLE_MEDUSA_ADMIN` environment variable for each Medusa application instance. + +### Configure Redis URL + +The `redisUrl` configuration specifies the connection URL to Redis to store the Medusa server's session. + +Learn more in the [Medusa Configuration documentation](https://docs.medusajs.com/learn/configurations/medusa-config#redisurl/index.html.md). + +So, add the following configuration in `medusa-config.ts` : + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + // ... + redisUrl: process.env.REDIS_URL, + }, +}) +``` + +*** + +## 2. Add predeploy Script + +Before you start the Medusa application in production, you should always run migrations and sync links. + +So, add the following script in `package.json`: + +```json +"scripts": { + // ... + "predeploy": "medusa db:migrate" +}, +``` + +*** + +## 3. Install Production Modules and Providers + +By default, your Medusa application uses modules and providers useful for development, such as the In-Memory Cache Module or the Local File Module Provider. + +It’s highly recommended to instead use modules and providers suitable for production, including: + +- [Redis Cache Module](https://docs.medusajs.com/resources/infrastructure-modules/cache/redis/index.html.md) +- [Redis Event Bus Module](https://docs.medusajs.com/resources/infrastructure-modules/event/redis/index.html.md) +- [Workflow Engine Redis Module](https://docs.medusajs.com/resources/infrastructure-modules/workflow-engine/redis/index.html.md) +- [S3 File Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/file/s3/index.html.md) (or other file module providers production-ready). +- [SendGrid Notification Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/notification/sendgrid/index.html.md) (or other notification module providers production-ready). + +Then, add these modules in `medusa-config.ts`: + +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/cache-redis", + options: { + redisUrl: process.env.REDIS_URL, + }, + }, + { + resolve: "@medusajs/medusa/event-bus-redis", + options: { + redisUrl: process.env.REDIS_URL, + }, + }, + { + resolve: "@medusajs/medusa/workflow-engine-redis", + options: { + redis: { + url: process.env.REDIS_URL, + }, + }, + }, + ], +}) +``` + +Check out the [Integrations](https://docs.medusajs.com/resources/integrations/index.html.md) and [Infrastructure Modules](https://docs.medusajs.com/resources/infrastructure-modules/index.html.md) documentation for other modules and providers to use. + +*** + +## 4. Create PostgreSQL and Redis Databases + +Your Medusa application must connect to PostgreSQL and Redis databases. So, before you deploy it, create production PostgreSQL and Redis databases. + +If your hosting provider doesn't support databases, you can use [Neon for PostgreSQL database hosting](https://neon.tech/), and [Redis Cloud for the Redis database hosting](https://redis.io/cloud/). + +After hosting both databases, keep their connection URLs for the next steps. + +*** + +## 5. Deploy Medusa Application in Server Mode + +As mentioned earlier, you'll deploy two instances or create two deployments of your Medusa application: one in server mode, and the other in worker mode. + +The deployment steps depend on your hosting provider. This section provides the general steps to perform during the deployment. + +### Set Environment Variables + +When setting the environment variables of the Medusa application, set the following variables: + +```bash +COOKIE_SECRET=supersecret # TODO GENERATE SECURE SECRET +JWT_SECRET=supersecret # TODO GENERATE SECURE SECRET +STORE_CORS= # STOREFRONT URL +ADMIN_CORS= # ADMIN URL +AUTH_CORS= # STOREFRONT AND ADMIN URLS, SEPARATED BY COMMAS +DISABLE_MEDUSA_ADMIN=false +MEDUSA_WORKER_MODE=server +PORT=9000 +DATABASE_URL # POSTGRES DATABASE URL +REDIS_URL= # REDIS DATABASE URL +``` + +Where: + +- The value of `COOKIE_SECRET` and `JWT_SECRET` must be a randomly generated secret. +- `STORE_CORS`'s value is the URL of your storefront. If you don’t have it yet, you can skip adding it for now. +- `ADMIN_CORS`'s value is the URL of the admin dashboard, which is the same as the server Medusa application. You can add it later if you don't currently have it. +- `AUTH_CORS`'s value is the URLs of any application authenticating users, customers, or other actor types, such as the storefront and admin URLs. The URLs are separated by commas. If you don’t have the URLs yet, you can set its value later. +- Set `DISABLE_MEDUSA_ADMIN`'s value to `false` so that the admin is built with the server application. +- Set the PostgreSQL database's connection URL as the value of `DATABASE_URL` +- Set the Redis database's connection URL as the value of `REDIS_URL`. + +Feel free to add any other relevant environment variables, such as for integrations and Infrastructure Modules. If you're using environment variables in your admin customizations, make sure to set them as well, as they're inlined during the build process. + +### Set Start Command + +The Medusa application's production build, which is created using the `build` command, outputs the Medusa application to `.medusa/server`. So, you must install the dependencies in the `.medusa/server` directory, then run the `start` command in it. + +If your hosting provider doesn't support setting a current-working directory, set the start command to the following: + +```bash npm2yarn +cd .medusa/server && npm install && npm run predeploy && npm run start +``` + +Notice that you run the `predeploy` command before starting the Medusa application to run migrations and sync links whenever there's an update. + +### Set Backend URL in Admin Configuration + +The Medusa Admin is built and hosted statically. To send requests to the Medusa server application, you must set the backend URL in the Medusa Admin's configuration. + +After you’ve obtained the Medusa application’s URL, add the following configuration to `medusa-config.ts`: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + admin: { + // ... + backendUrl: process.env.MEDUSA_BACKEND_URL, + }, +}) +``` + +Then, push the changes to the GitHub repository or deployed application. + +In your hosting provider, add or modify the following environment variables for the Medusa application in server mode: + +```bash +ADMIN_CORS= # MEDUSA APPLICATION URL +AUTH_CORS= # ADD MEDUSA APPLICATION URL +MEDUSA_BACKEND_URL= # URL TO DEPLOYED MEDUSA APPLICATION +``` + +Where you set the value of `ADMIN_CORS` and `MEDUSA_BACKEND_URL` to the Medusa application’s URL, and you add the URL to `AUTH_CORS`. + +After setting the environment variables, make sure to restart the deployment for the changes to take effect. + +Remember to separate URLs in `AUTH_CORS` by commas. + +*** + +## 6. Deploy Medusa Application in Worker Mode + +Next, you'll deploy the Medusa application in worker mode. + +As explained in the previous section, the deployment steps depend on your hosting provider. This section provides the general steps to perform during the deployment. + +### Set Environment Variables + +When setting the environment variables of the Medusa application, set the following variables: + +```bash +COOKIE_SECRET=supersecret # TODO GENERATE SECURE SECRET +JWT_SECRET=supersecret # TODO GENERATE SECURE SECRET +DISABLE_MEDUSA_ADMIN=true +MEDUSA_WORKER_MODE=worker +PORT=9000 +DATABASE_URL # POSTGRES DATABASE URL +REDIS_URL= # REDIS DATABASE URL +``` + +Where: + +- The value of `COOKIE_SECRET` and `JWT_SECRET` must be a randomly generated secret. +- Set `DISABLE_MEDUSA_ADMIN`'s value to `true` so that the admin isn't built with the worker application. +- Set the PostgreSQL database's connection URL as the value of `DATABASE_URL` +- Set the Redis database's connection URL as the value of `REDIS_URL`. + +Feel free to add any other relevant environment variables, such as for integrations and Infrastructure Modules. + +### Set Start Command + +The Medusa application's production build, which is created using the `build` command, outputs the Medusa application to `.medusa/server`. So, you must install the dependencies in the `.medusa/server` directory, then run the `start` command in it. + +If your hosting provider doesn't support setting a current-working directory, set the start command to the following: + +```bash npm2yarn +cd .medusa/server && npm run install && npm run start +``` + +*** + +## 7. Test Deployed Application + +Once the application is deployed and live, go to `/health`, where `` is the URL of the Medusa application in server mode. If the deployment was successful, you’ll see the `OK` response. + +The Medusa Admin is also available at `/app`. + +*** + +## Create Admin User + +If your hosting provider supports running commands in your Medusa application's directory, run the following command to create an admin user: + +```bash +npx medusa user -e admin-medusa@test.com -p supersecret +``` + +Replace the email `admin-medusa@test.com` and password `supersecret` with the credentials you want. + +You can use these credentials to log into the Medusa Admin dashboard. # Medusa Application Configuration @@ -1467,29 +2015,6 @@ npx medusa db:migrate ``` -# 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. - - # Using TypeScript Aliases By default, Medusa doesn't support TypeScript aliases in production. @@ -1534,658 +2059,133 @@ import { BrandModuleService } from "@/modules/brand/service" ``` -# Configure Instrumentation +# Build Custom Features -In this chapter, you'll learn about observability in Medusa and how to configure instrumentation with OpenTelemetry. +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. -## Observability with OpenTelemtry +By following these guides, you'll add brands to the Medusa application that you can associate with products. -Medusa uses [OpenTelemetry](https://opentelemetry.io/) for instrumentation and reporting. When configured, it reports traces for: +To build a custom feature in Medusa, you need three main tools: -- HTTP requests -- Workflow executions -- Query usages -- Database queries and operations +- [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 Configure Instrumentation in Medusa? +## Next Chapters: Brand Module Example -### Prerequisites +The next chapters will guide you to: -- [An exporter to visualize your application's traces, such as Zipkin.](https://zipkin.io/pages/quickstart.html) +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. -### Install Dependencies -Start by installing the following OpenTelemetry dependencies in your Medusa project: +# Customize Medusa Admin Dashboard -```bash npm2yarn -npm install @opentelemetry/sdk-node @opentelemetry/resources @opentelemetry/sdk-trace-node @opentelemetry/instrumentation-pg -``` +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). -Also, install the dependencies relevant for the exporter you use. If you're using Zipkin, install the following dependencies: +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: -```bash npm2yarn -npm install @opentelemetry/exporter-zipkin -``` +- 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). -### 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. +From these customizations, you can send requests to custom API routes, allowing admin users to manage custom resources on the dashboard *** -## Test it Out +## Next Chapters: View Brands in Dashboard -To test it out, start your exporter, such as Zipkin. +In the next chapters, you'll continue with the brands example to: -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. +- 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. -# Logging +# Extend Core Commerce Features -In this chapter, you’ll learn how to use Medusa’s logging utility. +In the upcoming chapters, you'll learn about the concepts and tools to extend Medusa's core commerce features. -## Logger Class +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. -Medusa provides a `Logger` class with advanced logging functionalities. This includes configuring logging levels or saving logs to a file. +The Medusa Framework and orchestration tools mitigate these issues while supporting all your customization needs: -The Medusa application registers the `Logger` class in the Medusa container and each module's container as `logger`. +- [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. *** -## How to Log a Message +## Next Chapters: Link Brands to Products Example -Resolve the `logger` using the Medusa container to log a message in your resource. +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, create the file `src/jobs/log-message.ts` with the following content: +- 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. -```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) +# Customizations Next Steps: Learn the Fundamentals - logger.info("I'm using the logger!") -} +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. -export const config = { - name: "test-logger", - // execute every minute - schedule: "* * * * *", -} -``` +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. -This creates a scheduled job that resolves the `logger` from the Medusa container and uses it to log a message. +## Useful Guides -### Test the Scheduled Job +The following guides and references are useful for your development journey: -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! -``` +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. *** -## Log Levels +## More Examples in Recipes -The `Logger` class has the following methods: +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. -- `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. +# Re-Use Customizations with Plugins + +In the previous chapters, you've learned important concepts related to creating modules, implementing commerce features in workflows, exposing those features in API routes, customizing the Medusa Admin dashboard with Admin Extensions, and integrating third-party systems. + +You've implemented the brands example within a single Medusa application. However, this approach is not scalable when you want to reuse your customizations across multiple projects. + +To reuse your customizations across multiple Medusa applications, such as implementing brands in different projects, you can create a plugin. A plugin is an NPM package that encapsulates your customizations and can be installed in any Medusa application. Plugins can include modules, workflows, API routes, Admin Extensions, and more. + +![Diagram showcasing how the Brand Plugin would add its resources to any application it's installed in](https://res.cloudinary.com/dza7lstvk/image/upload/v1737540091/Medusa%20Book/brand-plugin_bk9zi9.jpg) + +Medusa provides the tooling to create a plugin package, test it in a local Medusa application, and publish it to NPM. + +To learn more about plugins and how to create them, refer to [this chapter](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). + + +# Integrate Third-Party Systems + +Commerce applications often connect to third-party systems that provide additional or specialized features. For example, you may integrate a Content-Management System (CMS) for rich content features, a payment provider to process credit-card payments, and a notification service to send emails. + +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. *** -## Logging Configurations +## Next Chapters: Sync Brands Example -### Log Level +In the previous chapters, you've [added brands](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) to your Medusa application. In the next chapters, you will: -The 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. - - -# General Medusa Application Deployment Guide - -In this document, you'll learn the general steps to deploy your Medusa application. How you apply these steps depend on your chosen hosting provider or platform. - -Find how-to guides for specific platforms in [this documentation](https://docs.medusajs.com/resources/deployment/index.html.md). - -Want Medusa to manage and maintain your infrastructure? [Sign up and learn more about Medusa Cloud](https://medusajs.com/pricing) - -Medusa Cloud is our managed services offering that makes deploying and operating Medusa applications possible without having to worry about configuring, scaling, and maintaining infrastructure. Medusa Cloud hosts your server, Admin dashboard, database, and Redis instance. - -With Medusa Cloud, you maintain full customization control as you deploy your own modules and customizations directly from GitHub: - -- Push to deploy. -- Multiple testing environments. -- Preview environments for new PRs. -- Test on production-like data. - -### Prerequisites - -- [Medusa application’s codebase hosted on GitHub repository.](https://docs.medusajs.com/learn/index.html.md) - -## What You'll Deploy - -When you deploy the Medusa application, you need to deploy the following resources: - -1. PostgreSQL database: This is the database that will hold your Medusa application's details. -2. Redis database: This is the database that will store the Medusa server's session. -3. Medusa application in [server and worker mode](https://docs.medusajs.com/learn/production/worker-mode/index.html.md), where: - - The server mode handles incoming API requests and serving the Medusa Admin dashboard. - - The worker mode handles background tasks, such as scheduled jobs and subscribers. - -So, when choosing a hosting provider, make sure it supports deploying these resources. Also, for optimal experience, the hosting provider and plan must offer at least 2GB of RAM. - -*** - -## 1. Configure Medusa Application - -### Worker Mode - -The `workerMode` configuration determines which mode the Medusa application runs in. When you deploy the Medusa application, you deploy two instances: one in server mode, and one in worker mode. - -Learn more about worker mode in the [Worker Module chapter](https://docs.medusajs.com/learn/production/worker-mode/index.html.md). - -So, add the following configuration in `medusa-config.ts`: - -```ts title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - // ... - workerMode: process.env.MEDUSA_WORKER_MODE as "shared" | "worker" | "server", - }, -}) -``` - -Later, you’ll set different values of the `MEDUSA_WORKER_MODE` environment variable for each Medusa application deployment or instance. - -### Configure Medusa Admin - -The Medusa Admin is served by the Medusa server application. So, you need to disable it in the worker Medusa application only. - -To disable the Medusa Admin in the worker Medusa application while keeping it enabled in the server Medusa application, add the following configuration in `medusa-config.ts`: - -```ts title="medusa-config.ts" -module.exports = defineConfig({ - // ... - admin: { - disable: process.env.DISABLE_MEDUSA_ADMIN === "true", - }, -}) -``` - -Later, you’ll set different values of the `DISABLE_MEDUSA_ADMIN` environment variable for each Medusa application instance. - -### Configure Redis URL - -The `redisUrl` configuration specifies the connection URL to Redis to store the Medusa server's session. - -Learn more in the [Medusa Configuration documentation](https://docs.medusajs.com/learn/configurations/medusa-config#redisurl/index.html.md). - -So, add the following configuration in `medusa-config.ts` : - -```ts title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - // ... - redisUrl: process.env.REDIS_URL, - }, -}) -``` - -*** - -## 2. Add predeploy Script - -Before you start the Medusa application in production, you should always run migrations and sync links. - -So, add the following script in `package.json`: - -```json -"scripts": { - // ... - "predeploy": "medusa db:migrate" -}, -``` - -*** - -## 3. Install Production Modules and Providers - -By default, your Medusa application uses modules and providers useful for development, such as the In-Memory Cache Module or the Local File Module Provider. - -It’s highly recommended to instead use modules and providers suitable for production, including: - -- [Redis Cache Module](https://docs.medusajs.com/resources/infrastructure-modules/cache/redis/index.html.md) -- [Redis Event Bus Module](https://docs.medusajs.com/resources/infrastructure-modules/event/redis/index.html.md) -- [Workflow Engine Redis Module](https://docs.medusajs.com/resources/infrastructure-modules/workflow-engine/redis/index.html.md) -- [S3 File Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/file/s3/index.html.md) (or other file module providers production-ready). -- [SendGrid Notification Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/notification/sendgrid/index.html.md) (or other notification module providers production-ready). - -Then, add these modules in `medusa-config.ts`: - -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" - -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/cache-redis", - options: { - redisUrl: process.env.REDIS_URL, - }, - }, - { - resolve: "@medusajs/medusa/event-bus-redis", - options: { - redisUrl: process.env.REDIS_URL, - }, - }, - { - resolve: "@medusajs/medusa/workflow-engine-redis", - options: { - redis: { - url: process.env.REDIS_URL, - }, - }, - }, - ], -}) -``` - -Check out the [Integrations](https://docs.medusajs.com/resources/integrations/index.html.md) and [Infrastructure Modules](https://docs.medusajs.com/resources/infrastructure-modules/index.html.md) documentation for other modules and providers to use. - -*** - -## 4. Create PostgreSQL and Redis Databases - -Your Medusa application must connect to PostgreSQL and Redis databases. So, before you deploy it, create production PostgreSQL and Redis databases. - -If your hosting provider doesn't support databases, you can use [Neon for PostgreSQL database hosting](https://neon.tech/), and [Redis Cloud for the Redis database hosting](https://redis.io/cloud/). - -After hosting both databases, keep their connection URLs for the next steps. - -*** - -## 5. Deploy Medusa Application in Server Mode - -As mentioned earlier, you'll deploy two instances or create two deployments of your Medusa application: one in server mode, and the other in worker mode. - -The deployment steps depend on your hosting provider. This section provides the general steps to perform during the deployment. - -### Set Environment Variables - -When setting the environment variables of the Medusa application, set the following variables: - -```bash -COOKIE_SECRET=supersecret # TODO GENERATE SECURE SECRET -JWT_SECRET=supersecret # TODO GENERATE SECURE SECRET -STORE_CORS= # STOREFRONT URL -ADMIN_CORS= # ADMIN URL -AUTH_CORS= # STOREFRONT AND ADMIN URLS, SEPARATED BY COMMAS -DISABLE_MEDUSA_ADMIN=false -MEDUSA_WORKER_MODE=server -PORT=9000 -DATABASE_URL # POSTGRES DATABASE URL -REDIS_URL= # REDIS DATABASE URL -``` - -Where: - -- The value of `COOKIE_SECRET` and `JWT_SECRET` must be a randomly generated secret. -- `STORE_CORS`'s value is the URL of your storefront. If you don’t have it yet, you can skip adding it for now. -- `ADMIN_CORS`'s value is the URL of the admin dashboard, which is the same as the server Medusa application. You can add it later if you don't currently have it. -- `AUTH_CORS`'s value is the URLs of any application authenticating users, customers, or other actor types, such as the storefront and admin URLs. The URLs are separated by commas. If you don’t have the URLs yet, you can set its value later. -- Set `DISABLE_MEDUSA_ADMIN`'s value to `false` so that the admin is built with the server application. -- Set the PostgreSQL database's connection URL as the value of `DATABASE_URL` -- Set the Redis database's connection URL as the value of `REDIS_URL`. - -Feel free to add any other relevant environment variables, such as for integrations and Infrastructure Modules. If you're using environment variables in your admin customizations, make sure to set them as well, as they're inlined during the build process. - -### Set Start Command - -The Medusa application's production build, which is created using the `build` command, outputs the Medusa application to `.medusa/server`. So, you must install the dependencies in the `.medusa/server` directory, then run the `start` command in it. - -If your hosting provider doesn't support setting a current-working directory, set the start command to the following: - -```bash npm2yarn -cd .medusa/server && npm install && npm run predeploy && npm run start -``` - -Notice that you run the `predeploy` command before starting the Medusa application to run migrations and sync links whenever there's an update. - -### Set Backend URL in Admin Configuration - -The Medusa Admin is built and hosted statically. To send requests to the Medusa server application, you must set the backend URL in the Medusa Admin's configuration. - -After you’ve obtained the Medusa application’s URL, add the following configuration to `medusa-config.ts`: - -```ts title="medusa-config.ts" -module.exports = defineConfig({ - // ... - admin: { - // ... - backendUrl: process.env.MEDUSA_BACKEND_URL, - }, -}) -``` - -Then, push the changes to the GitHub repository or deployed application. - -In your hosting provider, add or modify the following environment variables for the Medusa application in server mode: - -```bash -ADMIN_CORS= # MEDUSA APPLICATION URL -AUTH_CORS= # ADD MEDUSA APPLICATION URL -MEDUSA_BACKEND_URL= # URL TO DEPLOYED MEDUSA APPLICATION -``` - -Where you set the value of `ADMIN_CORS` and `MEDUSA_BACKEND_URL` to the Medusa application’s URL, and you add the URL to `AUTH_CORS`. - -After setting the environment variables, make sure to restart the deployment for the changes to take effect. - -Remember to separate URLs in `AUTH_CORS` by commas. - -*** - -## 6. Deploy Medusa Application in Worker Mode - -Next, you'll deploy the Medusa application in worker mode. - -As explained in the previous section, the deployment steps depend on your hosting provider. This section provides the general steps to perform during the deployment. - -### Set Environment Variables - -When setting the environment variables of the Medusa application, set the following variables: - -```bash -COOKIE_SECRET=supersecret # TODO GENERATE SECURE SECRET -JWT_SECRET=supersecret # TODO GENERATE SECURE SECRET -DISABLE_MEDUSA_ADMIN=true -MEDUSA_WORKER_MODE=worker -PORT=9000 -DATABASE_URL # POSTGRES DATABASE URL -REDIS_URL= # REDIS DATABASE URL -``` - -Where: - -- The value of `COOKIE_SECRET` and `JWT_SECRET` must be a randomly generated secret. -- Set `DISABLE_MEDUSA_ADMIN`'s value to `true` so that the admin isn't built with the worker application. -- Set the PostgreSQL database's connection URL as the value of `DATABASE_URL` -- Set the Redis database's connection URL as the value of `REDIS_URL`. - -Feel free to add any other relevant environment variables, such as for integrations and Infrastructure Modules. - -### Set Start Command - -The Medusa application's production build, which is created using the `build` command, outputs the Medusa application to `.medusa/server`. So, you must install the dependencies in the `.medusa/server` directory, then run the `start` command in it. - -If your hosting provider doesn't support setting a current-working directory, set the start command to the following: - -```bash npm2yarn -cd .medusa/server && npm run install && npm run start -``` - -*** - -## 7. Test Deployed Application - -Once the application is deployed and live, go to `/health`, where `` is the URL of the Medusa application in server mode. If the deployment was successful, you’ll see the `OK` response. - -The Medusa Admin is also available at `/app`. - -*** - -## Create Admin User - -If your hosting provider supports running commands in your Medusa application's directory, run the following command to create an admin user: - -```bash -npx medusa user -e admin-medusa@test.com -p supersecret -``` - -Replace the email `admin-medusa@test.com` and password `supersecret` with the credentials you want. - -You can use these credentials to log into the Medusa Admin dashboard. - - -# 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. # Custom CLI Scripts @@ -2343,108 +2343,83 @@ curl http://localhost:9000/hello-world You're exposing custom functionality to be used by a storefront, admin dashboard, or any external application. -# 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 [this documentation](https://docs.medusajs.com/learn/fundamentals/admin/environment-variables/index.html.md). *** -## 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| +|---|---|---|---|---| +| +| +| +| +||The URL to connect to the PostgreSQL database. Only used if || +||URLs of storefronts that can access the Medusa backend's Store APIs. Only used if || +||URLs of admin dashboards that can access the Medusa backend's Admin APIs. Only used if || +||URLs of clients that can access the Medusa backend's authentication routes. Only used if || +||A random string used to create authentication tokens in the http layer. Only used if || +||A random string used to create cookie tokens in the http layer. Only used if || +||The URL to the Medusa backend. Only used if || +| +| +| +| +| +| +| +| +||The allowed levels to log. Learn more in || +||The file to save logs in. By default, logs aren't saved in any file. Learn more in || +||Whether to disable analytics data collection. Learn more in || # Events and Subscribers @@ -2549,83 +2524,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 @@ -3801,173 +3801,6 @@ For example, in a plugin, you can define a module that integrates a third-party The next chapter explains how you can create and publish a plugin. -# Scheduled Jobs - -In this chapter, you’ll learn about scheduled jobs and how to use them. - -## What is a Scheduled Job? - -When building your commerce application, you may need to automate tasks and run them repeatedly at a specific schedule. For example, you need to automatically sync products to a third-party service once a day. - -In other commerce platforms, this feature isn't natively supported. Instead, you have to setup a separate application to execute cron jobs, which adds complexity as to how you expose this task to be executed in a cron job, or how do you debug it when it's not running within the platform's tooling. - -Medusa removes this overhead by supporting this feature natively with scheduled jobs. A scheduled job is an asynchronous function that the Medusa application runs at the interval you specify during the Medusa application's runtime. Your efforts are only spent on implementing the functionality performed by the job, such as syncing products to an ERP. - -- You want the action to execute at a specified schedule while the Medusa application **isn't** running. Instead, use the operating system's equivalent of a cron job. -- You want to execute the action once when the application loads. Use [loaders](https://docs.medusajs.com/learn/fundamentals/modules/loaders/index.html.md) instead. -- You want to execute the action if an event occurs. Use [subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) instead. - -*** - -## How to Create a Scheduled Job? - -You create a scheduled job in a TypeScript or JavaScript file under the `src/jobs` directory. The file exports the asynchronous function to run, and the configurations indicating the schedule to run the function. - -For example, create the file `src/jobs/hello-world.ts` with the following content: - -![Example of scheduled job file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732866423/Medusa%20Book/scheduled-job-dir-overview_ediqgm.jpg) - -```ts title="src/jobs/hello-world.ts" highlights={highlights} -import { MedusaContainer } from "@medusajs/framework/types" - -export default async function greetingJob(container: MedusaContainer) { - const logger = container.resolve("logger") - - logger.info("Greeting!") -} - -export const config = { - name: "greeting-every-minute", - schedule: "* * * * *", -} -``` - -You export an asynchronous function that receives the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) as a parameter. In the function, you resolve the [Logger utility](https://docs.medusajs.com/learn/debugging-and-testing/logging/index.html.md) from the Medusa container and log a message. - -You also export a `config` object that has the following properties: - -- `name`: A unique name for the job. -- `schedule`: A string that holds a [cron expression](https://crontab.guru/) indicating the schedule to run the job. - -This scheduled job executes every minute and logs into the terminal `Greeting!`. - -### Test the Scheduled Job - -To test out your scheduled job, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -After a minute, the following message will be logged to the terminal: - -```bash -info: Greeting! -``` - -*** - -## Example: Sync Products Once a Day - -In this section, you'll find a brief example of how you use a scheduled job to sync products to a third-party service. - -When implementing flows spanning across systems or [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), you use [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). A workflow is a task made up of a series of steps, and you construct it like you would a regular function, but it's a special function that supports rollback mechanism in case of errors, background execution, and more. - -You can learn how to create a workflow in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), but this example assumes you already have a `syncProductToErpWorkflow` implemented. To execute this workflow once a day, create a scheduled job at `src/jobs/sync-products.ts` with the following content: - -```ts title="src/jobs/sync-products.ts" -import { MedusaContainer } from "@medusajs/framework/types" -import { syncProductToErpWorkflow } from "../workflows/sync-products-to-erp" - -export default async function syncProductsJob(container: MedusaContainer) { - await syncProductToErpWorkflow(container) - .run() -} - -export const config = { - name: "sync-products-job", - schedule: "0 0 * * *", -} -``` - -In the scheduled job function, you execute the `syncProductToErpWorkflow` by invoking it and passing it the container, then invoking the `run` method. You also specify in the exported configurations the schedule `0 0 * * *` which indicates midnight time of every day. - -The next time you start the Medusa application, it will run this job every day at midnight. - - -# Medusa's Architecture - -In this chapter, you'll learn about the architectural layers in Medusa. - -Find the full architectural diagram at the [end of this chapter](#full-diagram-of-medusas-architecture). - -## HTTP, Workflow, and Module Layers - -Medusa is a headless commerce platform. So, storefronts, admin dashboards, and other clients consume Medusa's functionalities through its API routes. - -In a common Medusa application, requests go through four layers in the stack. In order of entry, those are: - -1. API Routes (HTTP): Our API Routes are the typical entry point. The Medusa server is based on Express.js, which handles incoming requests. It can also connect to a Redis database that stores the server session data. -2. Workflows: API Routes consume workflows that hold the opinionated business logic of your application. -3. Modules: Workflows use domain-specific modules for resource management. -4. Data store: Modules query the underlying datastore, which is a PostgreSQL database in common cases. - -These layers of stack can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). - -![This diagram illustrates the entry point of requests into the Medusa application through API routes. It shows a storefront and an admin that can send a request to the HTTP layer. The HTTP layer then uses workflows to handle the business logic. Finally, the workflows use modules to query and manipulate data in the data stores.](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175296/Medusa%20Book/http-layer_sroafr.jpg) - -*** - -## Database Layer - -The Medusa application injects into each module, including your [custom modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), a connection to the configured PostgreSQL database. Modules use that connection to read and write data to the database. - -Modules can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). - -![This diagram illustrates how modules connect to the database.](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175379/Medusa%20Book/db-layer_pi7tix.jpg) - -*** - -## Third-Party Integrations Layer - -Third-party services and systems are integrated through Medusa's Commerce and Infrastructure Modules. You also create custom third-party integrations through a [custom module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). - -Modules can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). - -### Commerce Modules - -[Commerce Modules](https://docs.medusajs.com/resources/commerce-modules/index.html.md) integrate third-party services relevant for commerce or user-facing features. For example, you can integrate [Stripe](https://docs.medusajs.com/resources/commerce-modules/payment/payment-provider/stripe/index.html.md) through a Payment Module Provider, or [ShipStation](https://docs.medusajs.com/resources/integrations/guides/shipstation/index.html.md) through a Fulfillment Module Provider. - -You can also integrate third-party services for custom functionalities. For example, you can integrate [Sanity](https://docs.medusajs.com/resources/integrations/guides/sanity/index.html.md) for rich CMS capabilities, or [Odoo](https://docs.medusajs.com/resources/recipes/erp/odoo/index.html.md) to sync your Medusa application with your ERP system. - -You can replace any of the third-party services mentioned above to build your preferred commerce ecosystem. - -![Diagram illustrating the Commerce Modules integration to third-party services](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175357/Medusa%20Book/service-commerce_qcbdsl.jpg) - -### Infrastructure Modules - -[Infrastructure Modules](https://docs.medusajs.com/resources/infrastructure-modules/index.html.md) integrate third-party services and systems that customize Medusa's infrastructure. Medusa has the following Infrastructure Modules: - -- [Cache Module](https://docs.medusajs.com/resources/infrastructure-modules/cache/index.html.md): Caches data that require heavy computation. You can integrate a custom module to handle the caching with services like Memcached, or use the existing [Redis Cache Module](https://docs.medusajs.com/resources/infrastructure-modules/cache/redis/index.html.md). -- [Event Module](https://docs.medusajs.com/resources/infrastructure-modules/event/index.html.md): A pub/sub system that allows you to subscribe to events and trigger them. You can integrate [Redis](https://docs.medusajs.com/resources/infrastructure-modules/event/redis/index.html.md) as the pub/sub system. -- [File Module](https://docs.medusajs.com/resources/infrastructure-modules/file/index.html.md): Manages file uploads and storage, such as upload of product images. You can integrate [AWS S3](https://docs.medusajs.com/resources/infrastructure-modules/file/s3/index.html.md) for file storage. -- [Locking Module](https://docs.medusajs.com/resources/infrastructure-modules/locking/index.html.md): Manages access to shared resources by multiple processes or threads, preventing conflict between processes and ensuring data consistency. You can integrate [Redis](https://docs.medusajs.com/resources/infrastructure-modules/locking/redis/index.html.md) for locking. -- [Notification Module](https://docs.medusajs.com/resources/infrastructure-modules/notification/index.html.md): Sends notifications to customers and users, such as for order updates or newsletters. You can integrate [SendGrid](https://docs.medusajs.com/resources/infrastructure-modules/notification/sendgrid/index.html.md) for sending emails. -- [Workflow Engine Module](https://docs.medusajs.com/resources/infrastructure-modules/workflow-engine/index.html.md): Orchestrates workflows that hold the business logic of your application. You can integrate [Redis](https://docs.medusajs.com/resources/infrastructure-modules/workflow-engine/redis/index.html.md) to orchestrate workflows. - -All of the third-party services mentioned above can be replaced to help you build your preferred architecture and ecosystem. - -![Diagram illustrating the Infrastructure Modules integration to third-party services and systems](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175342/Medusa%20Book/service-arch_ozvryw.jpg) - -*** - -## Full Diagram of Medusa's Architecture - -The following diagram illustrates Medusa's architecture including all its layers. - -![Full diagram illustrating Medusa's architecture combining all the different layers.](https://res.cloudinary.com/dza7lstvk/image/upload/v1727174897/Medusa%20Book/architectural-diagram-full.jpg) - - # Modules In this chapter, you’ll learn about modules and how to create them. @@ -4268,6 +4101,265 @@ 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. +# 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. + + +# Medusa's Architecture + +In this chapter, you'll learn about the architectural layers in Medusa. + +Find the full architectural diagram at the [end of this chapter](#full-diagram-of-medusas-architecture). + +## HTTP, Workflow, and Module Layers + +Medusa is a headless commerce platform. So, storefronts, admin dashboards, and other clients consume Medusa's functionalities through its API routes. + +In a common Medusa application, requests go through four layers in the stack. In order of entry, those are: + +1. API Routes (HTTP): Our API Routes are the typical entry point. The Medusa server is based on Express.js, which handles incoming requests. It can also connect to a Redis database that stores the server session data. +2. Workflows: API Routes consume workflows that hold the opinionated business logic of your application. +3. Modules: Workflows use domain-specific modules for resource management. +4. Data store: Modules query the underlying datastore, which is a PostgreSQL database in common cases. + +These layers of stack can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). + +![This diagram illustrates the entry point of requests into the Medusa application through API routes. It shows a storefront and an admin that can send a request to the HTTP layer. The HTTP layer then uses workflows to handle the business logic. Finally, the workflows use modules to query and manipulate data in the data stores.](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175296/Medusa%20Book/http-layer_sroafr.jpg) + +*** + +## Database Layer + +The Medusa application injects into each module, including your [custom modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), a connection to the configured PostgreSQL database. Modules use that connection to read and write data to the database. + +Modules can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). + +![This diagram illustrates how modules connect to the database.](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175379/Medusa%20Book/db-layer_pi7tix.jpg) + +*** + +## Third-Party Integrations Layer + +Third-party services and systems are integrated through Medusa's Commerce and Infrastructure Modules. You also create custom third-party integrations through a [custom module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). + +Modules can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). + +### Commerce Modules + +[Commerce Modules](https://docs.medusajs.com/resources/commerce-modules/index.html.md) integrate third-party services relevant for commerce or user-facing features. For example, you can integrate [Stripe](https://docs.medusajs.com/resources/commerce-modules/payment/payment-provider/stripe/index.html.md) through a Payment Module Provider, or [ShipStation](https://docs.medusajs.com/resources/integrations/guides/shipstation/index.html.md) through a Fulfillment Module Provider. + +You can also integrate third-party services for custom functionalities. For example, you can integrate [Sanity](https://docs.medusajs.com/resources/integrations/guides/sanity/index.html.md) for rich CMS capabilities, or [Odoo](https://docs.medusajs.com/resources/recipes/erp/odoo/index.html.md) to sync your Medusa application with your ERP system. + +You can replace any of the third-party services mentioned above to build your preferred commerce ecosystem. + +![Diagram illustrating the Commerce Modules integration to third-party services](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175357/Medusa%20Book/service-commerce_qcbdsl.jpg) + +### Infrastructure Modules + +[Infrastructure Modules](https://docs.medusajs.com/resources/infrastructure-modules/index.html.md) integrate third-party services and systems that customize Medusa's infrastructure. Medusa has the following Infrastructure Modules: + +- [Cache Module](https://docs.medusajs.com/resources/infrastructure-modules/cache/index.html.md): Caches data that require heavy computation. You can integrate a custom module to handle the caching with services like Memcached, or use the existing [Redis Cache Module](https://docs.medusajs.com/resources/infrastructure-modules/cache/redis/index.html.md). +- [Event Module](https://docs.medusajs.com/resources/infrastructure-modules/event/index.html.md): A pub/sub system that allows you to subscribe to events and trigger them. You can integrate [Redis](https://docs.medusajs.com/resources/infrastructure-modules/event/redis/index.html.md) as the pub/sub system. +- [File Module](https://docs.medusajs.com/resources/infrastructure-modules/file/index.html.md): Manages file uploads and storage, such as upload of product images. You can integrate [AWS S3](https://docs.medusajs.com/resources/infrastructure-modules/file/s3/index.html.md) for file storage. +- [Locking Module](https://docs.medusajs.com/resources/infrastructure-modules/locking/index.html.md): Manages access to shared resources by multiple processes or threads, preventing conflict between processes and ensuring data consistency. You can integrate [Redis](https://docs.medusajs.com/resources/infrastructure-modules/locking/redis/index.html.md) for locking. +- [Notification Module](https://docs.medusajs.com/resources/infrastructure-modules/notification/index.html.md): Sends notifications to customers and users, such as for order updates or newsletters. You can integrate [SendGrid](https://docs.medusajs.com/resources/infrastructure-modules/notification/sendgrid/index.html.md) for sending emails. +- [Workflow Engine Module](https://docs.medusajs.com/resources/infrastructure-modules/workflow-engine/index.html.md): Orchestrates workflows that hold the business logic of your application. You can integrate [Redis](https://docs.medusajs.com/resources/infrastructure-modules/workflow-engine/redis/index.html.md) to orchestrate workflows. + +All of the third-party services mentioned above can be replaced to help you build your preferred architecture and ecosystem. + +![Diagram illustrating the Infrastructure Modules integration to third-party services and systems](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175342/Medusa%20Book/service-arch_ozvryw.jpg) + +*** + +## Full Diagram of Medusa's Architecture + +The following diagram illustrates Medusa's architecture including all its layers. + +![Full diagram illustrating Medusa's architecture combining all the different layers.](https://res.cloudinary.com/dza7lstvk/image/upload/v1727174897/Medusa%20Book/architectural-diagram-full.jpg) + + +# 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 +``` + + # Workflows In this chapter, you’ll learn about workflows and how to define and execute them. @@ -4522,98 +4614,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. -# 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. @@ -4770,6 +4770,207 @@ The Next.js Starter is compatible with some Medusa integrations out-of-the-box, Refer to the [Next.js Starter reference](https://docs.medusajs.com/resources/nextjs-starter/index.html.md) for more details. +# 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. + +### Prerequisites + +- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) + +## medusaIntegrationTestRunner Utility + +The `medusaIntegrationTestRunner` is from Medusa's Testing Framework and it's used to create integration tests in your Medusa project. It runs a full Medusa application, allowing you test API routes, workflows, or other customizations. + +For example: + +```ts title="integration-tests/http/test.spec.ts" highlights={highlights} +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" + +medusaIntegrationTestRunner({ + testSuite: ({ api, getContainer }) => { + // TODO write tests... + }, +}) + +jest.setTimeout(60 * 1000) +``` + +The `medusaIntegrationTestRunner` function accepts an object as a parameter. The object has a required property `testSuite`. + +`testSuite`'s value is a function that defines the tests to run. The function accepts as a parameter an object that has the following properties: + +- `api`: a set of utility methods used to send requests to the Medusa application. It has the following methods: + - `get`: Send a `GET` request to an API route. + - `post`: Send a `POST` request to an API route. + - `delete`: Send a `DELETE` request to an API route. +- `getContainer`: a function that retrieves the Medusa Container. Use the `getContainer().resolve` method to resolve resources from the Medusa Container. + +The tests in the `testSuite` function are written using [Jest](https://jestjs.io/). + +### Jest Timeout + +Since your tests connect to the database and perform actions that require more time than the typical tests, make sure to increase the timeout in your test: + +```ts title="integration-tests/http/test.spec.ts" +// in your test's file +jest.setTimeout(60 * 1000) +``` + +*** + +### Run Tests + +Run the following command to run your tests: + +```bash npm2yarn +npm run test:integration +``` + +If you don't have a `test:integration` script in `package.json`, refer to the [Medusa Testing Tools chapter](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools#add-test-commands/index.html.md). + +This runs your Medusa application and runs the tests available under the `src/integrations/http` directory. + +*** + +## Other Options and Inputs + +Refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/medusaIntegrationTestRunner/index.html.md) for other available parameter options and inputs of the `testSuite` function. + +*** + +## Database Used in Tests + +The `medusaIntegrationTestRunner` function creates a database with a random name before running the tests. Then, it drops that database after all the tests end. + +To manage that database, such as changing its name or perform operations on it in your tests, refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/medusaIntegrationTestRunner/index.html.md). + +*** + +## Example Integration Tests + +The next chapters provide examples of writing integration tests for API routes and workflows. + + +# Write Tests for Modules + +In this chapter, you'll learn about `moduleIntegrationTestRunner` from Medusa's Testing Framework and how to use it to write integration tests for a module's main service. + +### Prerequisites + +- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) + +## moduleIntegrationTestRunner Utility + +`moduleIntegrationTestRunner` creates integration tests for a module. The integration tests run on a test Medusa application with only the specified module enabled. + +For example, assuming you have a `blog` module, create a test file at `src/modules/blog/__tests__/service.spec.ts`: + +```ts title="src/modules/blog/__tests__/service.spec.ts" +import { moduleIntegrationTestRunner } from "@medusajs/test-utils" +import { BLOG_MODULE } from ".." +import BlogModuleService from "../service" +import Post from "../models/post" + +moduleIntegrationTestRunner({ + moduleName: BLOG_MODULE, + moduleModels: [Post], + resolve: "./src/modules/blog", + testSuite: ({ service }) => { + // TODO write tests + }, +}) + +jest.setTimeout(60 * 1000) +``` + +The `moduleIntegrationTestRunner` function accepts as a parameter an object with the following properties: + +- `moduleName`: The name of the module. +- `moduleModels`: An array of models in the module. Refer to [this section](#write-tests-for-modules-without-data-models) if your module doesn't have data models. +- `resolve`: The path to the module's directory. +- `testSuite`: A function that defines the tests to run. + +The `testSuite` function accepts as a parameter an object having the `service` property, which is an instance of the module's main service. + +The type argument provided to the `moduleIntegrationTestRunner` function is used as the type of the `service` property. + +The tests in the `testSuite` function are written using [Jest](https://jestjs.io/). + +*** + +## Run Tests + +Run the following command to run your module integration tests: + +```bash npm2yarn +npm run test:integration:modules +``` + +If you don't have a `test:integration:modules` script in `package.json`, refer to the [Medusa Testing Tools chapter](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools#add-test-commands/index.html.md). + +This runs your Medusa application and runs the tests available in any `__tests__` directory under the `src/modules` directory. + +*** + +## Pass Module Options + +If your module accepts options, you can set them using the `moduleOptions` property of the `moduleIntegrationTestRunner`'s parameter. + +For example: + +```ts +import { moduleIntegrationTestRunner } from "@medusajs/test-utils" +import BlogModuleService from "../service" + +moduleIntegrationTestRunner({ + moduleOptions: { + apiKey: "123", + }, + // ... +}) +``` + +*** + +## Write Tests for Modules without Data Models + +If your module doesn't have a data model, pass a dummy model in the `moduleModels` property. + +For example: + +```ts +import { moduleIntegrationTestRunner } from "@medusajs/test-utils" +import BlogModuleService from "../service" +import { model } from "@medusajs/framework/utils" + +const DummyModel = model.define("dummy_model", { + id: model.id().primaryKey(), +}) + +moduleIntegrationTestRunner({ + moduleModels: [DummyModel], + // ... +}) + +jest.setTimeout(60 * 1000) +``` + +*** + +### Other Options and Inputs + +Refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/moduleIntegrationTestRunner/index.html.md) for other available parameter options and inputs of the `testSuite` function. + +*** + +## Database Used in Tests + +The `moduleIntegrationTestRunner` function creates a database with a random name before running the tests. Then, it drops that database after all the tests end. + +To manage that database, such as changing its name or perform operations on it in your tests, refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/moduleIntegrationTestRunner/index.html.md). + + # 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. @@ -6809,88 +7010,6 @@ By following the previous chapters, you utilized the Medusa Framework and orches With Medusa, you can integrate any service from your commerce ecosystem with ease. You don't have to set up separate applications to manage your different customizations, or worry about data inconsistency across systems. Your efforts only go into implementing the business logic that ties your systems together. -# 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. - -### Prerequisites - -- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) - -## medusaIntegrationTestRunner Utility - -The `medusaIntegrationTestRunner` is from Medusa's Testing Framework and it's used to create integration tests in your Medusa project. It runs a full Medusa application, allowing you test API routes, workflows, or other customizations. - -For example: - -```ts title="integration-tests/http/test.spec.ts" highlights={highlights} -import { medusaIntegrationTestRunner } from "@medusajs/test-utils" - -medusaIntegrationTestRunner({ - testSuite: ({ api, getContainer }) => { - // TODO write tests... - }, -}) - -jest.setTimeout(60 * 1000) -``` - -The `medusaIntegrationTestRunner` function accepts an object as a parameter. The object has a required property `testSuite`. - -`testSuite`'s value is a function that defines the tests to run. The function accepts as a parameter an object that has the following properties: - -- `api`: a set of utility methods used to send requests to the Medusa application. It has the following methods: - - `get`: Send a `GET` request to an API route. - - `post`: Send a `POST` request to an API route. - - `delete`: Send a `DELETE` request to an API route. -- `getContainer`: a function that retrieves the Medusa Container. Use the `getContainer().resolve` method to resolve resources from the Medusa Container. - -The tests in the `testSuite` function are written using [Jest](https://jestjs.io/). - -### Jest Timeout - -Since your tests connect to the database and perform actions that require more time than the typical tests, make sure to increase the timeout in your test: - -```ts title="integration-tests/http/test.spec.ts" -// in your test's file -jest.setTimeout(60 * 1000) -``` - -*** - -### Run Tests - -Run the following command to run your tests: - -```bash npm2yarn -npm run test:integration -``` - -If you don't have a `test:integration` script in `package.json`, refer to the [Medusa Testing Tools chapter](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools#add-test-commands/index.html.md). - -This runs your Medusa application and runs the tests available under the `src/integrations/http` directory. - -*** - -## Other Options and Inputs - -Refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/medusaIntegrationTestRunner/index.html.md) for other available parameter options and inputs of the `testSuite` function. - -*** - -## Database Used in Tests - -The `medusaIntegrationTestRunner` function creates a database with a random name before running the tests. Then, it drops that database after all the tests end. - -To manage that database, such as changing its name or perform operations on it in your tests, refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/medusaIntegrationTestRunner/index.html.md). - -*** - -## Example Integration Tests - -The next chapters provide examples of writing integration tests for API routes and workflows. - - # 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. @@ -7050,51 +7169,6 @@ You can now use the CMS Module's service to perform actions on the third-party C 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 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", -}) -``` - - # Seed Data with Custom CLI Script In this chapter, you'll learn how to seed data using a custom CLI script. @@ -7283,123 +7357,49 @@ 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. -# Write Tests for Modules +# Admin Development Constraints -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. +This chapter lists some constraints of admin widgets and UI routes. -### Prerequisites +## Arrow Functions -- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) +Widget and UI route components must be created as arrow functions. -## moduleIntegrationTestRunner Utility - -`moduleIntegrationTestRunner` creates integration tests for a module. The integration tests run on a test Medusa application with only the specified module enabled. - -For example, assuming you have a `blog` module, create a test file at `src/modules/blog/__tests__/service.spec.ts`: - -```ts title="src/modules/blog/__tests__/service.spec.ts" -import { moduleIntegrationTestRunner } from "@medusajs/test-utils" -import { BLOG_MODULE } from ".." -import BlogModuleService from "../service" -import Post from "../models/post" - -moduleIntegrationTestRunner({ - moduleName: BLOG_MODULE, - moduleModels: [Post], - resolve: "./src/modules/blog", - testSuite: ({ service }) => { - // TODO write tests - }, -}) - -jest.setTimeout(60 * 1000) -``` - -The `moduleIntegrationTestRunner` function accepts as a parameter an object with the following properties: - -- `moduleName`: The name of the module. -- `moduleModels`: An array of models in the module. Refer to [this section](#write-tests-for-modules-without-data-models) if your module doesn't have data models. -- `resolve`: The path to the module's directory. -- `testSuite`: A function that defines the tests to run. - -The `testSuite` function accepts as a parameter an object having the `service` property, which is an instance of the module's main service. - -The type argument provided to the `moduleIntegrationTestRunner` function is used as the type of the `service` property. - -The tests in the `testSuite` function are written using [Jest](https://jestjs.io/). - -*** - -## Run Tests - -Run the following command to run your module integration tests: - -```bash npm2yarn -npm run test:integration:modules -``` - -If you don't have a `test:integration:modules` script in `package.json`, refer to the [Medusa Testing Tools chapter](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools#add-test-commands/index.html.md). - -This runs your Medusa application and runs the tests available in any `__tests__` directory under the `src/modules` directory. - -*** - -## Pass Module Options - -If your module accepts options, you can set them using the `moduleOptions` property of the `moduleIntegrationTestRunner`'s parameter. - -For example: - -```ts -import { moduleIntegrationTestRunner } from "@medusajs/test-utils" -import BlogModuleService from "../service" - -moduleIntegrationTestRunner({ - moduleOptions: { - apiKey: "123", - }, +```ts highlights={arrowHighlights} +// Don't +function ProductWidget() { // ... -}) -``` +} -*** - -## Write Tests for Modules without Data Models - -If your module doesn't have a data model, pass a dummy model in the `moduleModels` property. - -For example: - -```ts -import { moduleIntegrationTestRunner } from "@medusajs/test-utils" -import BlogModuleService from "../service" -import { model } from "@medusajs/framework/utils" - -const DummyModel = model.define("dummy_model", { - id: model.id().primaryKey(), -}) - -moduleIntegrationTestRunner({ - moduleModels: [DummyModel], +// Do +const ProductWidget = () => { // ... -}) - -jest.setTimeout(60 * 1000) +} ``` *** -### Other Options and Inputs +## Widget Zone -Refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/moduleIntegrationTestRunner/index.html.md) for other available parameter options and inputs of the `testSuite` function. +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`, +}) -## Database Used in Tests +// Don't +const ZONE = "product.details.after" +export const config = defineWidgetConfig({ + zone: ZONE, +}) -The `moduleIntegrationTestRunner` function creates a database with a random name before running the tests. Then, it drops that database after all the tests end. - -To manage that database, such as changing its name or perform operations on it in your tests, refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/moduleIntegrationTestRunner/index.html.md). +// Do +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) +``` # Environment Variables in Admin Customizations @@ -7763,125 +7763,6 @@ export const handle = { Refer to [react-router-dom’s documentation](https://reactrouter.com/en/6.29.0) for components and hooks that you can use in your admin customizations. -# Admin 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. @@ -8118,6 +7999,125 @@ 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. + + # 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. @@ -8579,151 +8579,6 @@ This adds two API Routes: - A `POST` route at `http://localhost:9000/hello-world`. -# API Route Parameters - -In this chapter, you’ll learn about path, query, and request body parameters. - -## Path Parameters - -To create an API route that accepts a path parameter, create a directory within the route file's path whose name is of the format `[param]`. - -For example, to create an API Route at the path `/hello-world/:id`, where `:id` is a path parameter, create the file `src/api/hello-world/[id]/route.ts` with the following content: - -```ts title="src/api/hello-world/[id]/route.ts" highlights={singlePathHighlights} -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" - -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: `[GET] Hello ${req.params.id}!`, - }) -} -``` - -The `MedusaRequest` object has a `params` property. `params` holds the path parameters in key-value pairs. - -### Multiple Path Parameters - -To create an API route that accepts multiple path parameters, create within the file's path multiple directories whose names are of the format `[param]`. - -For example, to create an API route at `/hello-world/:id/name/:name`, create the file `src/api/hello-world/[id]/name/[name]/route.ts` with the following content: - -```ts title="src/api/hello-world/[id]/name/[name]/route.ts" highlights={multiplePathHighlights} -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" - -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: `[GET] Hello ${ - req.params.id - } - ${req.params.name}!`, - }) -} -``` - -You access the `id` and `name` path parameters using the `req.params` property. - -*** - -## Query Parameters - -You can access all query parameters in the `query` property of the `MedusaRequest` object. `query` is an object of key-value pairs, where the key is a query parameter's name, and the value is its value. - -For example: - -```ts title="src/api/hello-world/route.ts" highlights={queryHighlights} -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" - -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: `Hello ${req.query.name}`, - }) -} -``` - -The value of `req.query.name` is the value passed in `?name=John`, for example. - -### Validate Query Parameters - -You can apply validation rules on received query parameters to ensure they match specified rules and types. - -Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation#how-to-validate-request-query-paramters/index.html.md). - -*** - -## Request Body Parameters - -The Medusa application parses the body of any request having a JSON, URL-encoded, or text request content types. The request body parameters are set in the `MedusaRequest`'s `body` property. - -Learn more about configuring body parsing in [this guide](https://docs.medusajs.com/learn/fundamentals/api-routes/parse-body/index.html.md). - -For example: - -```ts title="src/api/hello-world/route.ts" highlights={bodyHighlights} -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" - -type HelloWorldReq = { - name: string -} - -export const POST = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: `[POST] Hello ${req.body.name}!`, - }) -} -``` - -In this example, you use the `name` request body parameter to create the message in the returned response. - -The `MedusaRequest` type accepts a type argument that indicates the type of the request body. This is useful for auto-completion and to avoid typing errors. - -To test it out, send the following request to your Medusa application: - -```bash -curl -X POST 'http://localhost:9000/hello-world' \ --H 'Content-Type: application/json' \ ---data-raw '{ - "name": "John" -}' -``` - -This returns the following JSON object: - -```json -{ - "message": "[POST] Hello John!" -} -``` - -### Validate Body Parameters - -You can apply validation rules on received body parameters to ensure they match specified rules and types. - -Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation#how-to-validate-request-body/index.html.md). - - # Middlewares In this chapter, you’ll learn about middlewares and how to create them. @@ -9072,6 +8927,353 @@ A middleware can not override an existing middleware. Instead, middlewares are a For example, if you define a custom validation middleware, such as `validateAndTransformBody`, on an existing route, then both the original and the custom validation middleware will run. +# API Route Parameters + +In this chapter, you’ll learn about path, query, and request body parameters. + +## Path Parameters + +To create an API route that accepts a path parameter, create a directory within the route file's path whose name is of the format `[param]`. + +For example, to create an API Route at the path `/hello-world/:id`, where `:id` is a path parameter, create the file `src/api/hello-world/[id]/route.ts` with the following content: + +```ts title="src/api/hello-world/[id]/route.ts" highlights={singlePathHighlights} +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" + +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: `[GET] Hello ${req.params.id}!`, + }) +} +``` + +The `MedusaRequest` object has a `params` property. `params` holds the path parameters in key-value pairs. + +### Multiple Path Parameters + +To create an API route that accepts multiple path parameters, create within the file's path multiple directories whose names are of the format `[param]`. + +For example, to create an API route at `/hello-world/:id/name/:name`, create the file `src/api/hello-world/[id]/name/[name]/route.ts` with the following content: + +```ts title="src/api/hello-world/[id]/name/[name]/route.ts" highlights={multiplePathHighlights} +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" + +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: `[GET] Hello ${ + req.params.id + } - ${req.params.name}!`, + }) +} +``` + +You access the `id` and `name` path parameters using the `req.params` property. + +*** + +## Query Parameters + +You can access all query parameters in the `query` property of the `MedusaRequest` object. `query` is an object of key-value pairs, where the key is a query parameter's name, and the value is its value. + +For example: + +```ts title="src/api/hello-world/route.ts" highlights={queryHighlights} +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" + +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: `Hello ${req.query.name}`, + }) +} +``` + +The value of `req.query.name` is the value passed in `?name=John`, for example. + +### Validate Query Parameters + +You can apply validation rules on received query parameters to ensure they match specified rules and types. + +Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation#how-to-validate-request-query-paramters/index.html.md). + +*** + +## Request Body Parameters + +The Medusa application parses the body of any request having a JSON, URL-encoded, or text request content types. The request body parameters are set in the `MedusaRequest`'s `body` property. + +Learn more about configuring body parsing in [this guide](https://docs.medusajs.com/learn/fundamentals/api-routes/parse-body/index.html.md). + +For example: + +```ts title="src/api/hello-world/route.ts" highlights={bodyHighlights} +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" + +type HelloWorldReq = { + name: string +} + +export const POST = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: `[POST] Hello ${req.body.name}!`, + }) +} +``` + +In this example, you use the `name` request body parameter to create the message in the returned response. + +The `MedusaRequest` type accepts a type argument that indicates the type of the request body. This is useful for auto-completion and to avoid typing errors. + +To test it out, send the following request to your Medusa application: + +```bash +curl -X POST 'http://localhost:9000/hello-world' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "name": "John" +}' +``` + +This returns the following JSON object: + +```json +{ + "message": "[POST] Hello John!" +} +``` + +### Validate Body Parameters + +You can apply validation rules on received body parameters to ensure they match specified rules and types. + +Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation#how-to-validate-request-body/index.html.md). + + +# Protected Routes + +In this chapter, you’ll learn how to create protected routes. + +## What is a Protected Route? + +A protected route is a route that requires requests to be user-authenticated before performing the route's functionality. Otherwise, the request fails, and the user is prevented access. + +*** + +## Default Protected Routes + +Medusa applies an authentication guard on routes starting with `/admin`, including custom API routes. + +Requests to `/admin` must be user-authenticated to access the route. + +Refer to the API Reference for [Admin](https://docs.medusajs.com/api/admin#authentication) and [Store](https://docs.medusajs.com/api/store#authentication) authentication methods. + +*** + +## Protect Custom API Routes + +To protect custom API Routes to only allow authenticated customer or admin users, use the `authenticate` middleware from the Medusa Framework. + +For example: + +```ts title="src/api/middlewares.ts" highlights={highlights} +import { + defineMiddlewares, + authenticate, +} from "@medusajs/framework/http" + +export default defineMiddlewares({ + routes: [ + { + matcher: "/custom/admin*", + middlewares: [authenticate("user", ["session", "bearer", "api-key"])], + }, + { + matcher: "/custom/customer*", + middlewares: [authenticate("customer", ["session", "bearer"])], + }, + ], +}) +``` + +The `authenticate` middleware function accepts three parameters: + +1. The type of user authenticating. Use `user` for authenticating admin users, and `customer` for authenticating customers. You can also pass `*` to allow all types of users, or pass an array of actor types. +2. An array of types of authentication methods allowed. Both `user` and `customer` scopes support `session` and `bearer`. The `admin` scope also supports the `api-key` authentication method. +3. An optional object of configurations accepting the following properties: + - `allowUnauthenticated`: (default: `false`) A boolean indicating whether authentication is required. For example, you may have an API route where you want to access the logged-in customer if available, but guest customers can still access it too. + - `allowUnregistered` (default: `false`): A boolean indicating if unregistered users should be allowed access. This is useful when you want to allow users who aren’t registered to access certain routes. + +### Example: Custom Actor Type + +For example, to require authentication of a custom actor type `manager` to an API route: + +```ts title="src/api/middlewares.ts" +import { + defineMiddlewares, + authenticate, +} from "@medusajs/framework/http" + +export default defineMiddlewares({ + routes: [ + { + matcher: "/manager*", + middlewares: [authenticate("manager", ["session", "bearer"])], + }, + ], +}) +``` + +Refer to the [Custom Actor-Type Guide](https://docs.medusajs.com/resources/commerce-modules/auth/create-actor-type/index.html.md) for detailed explanation on how to create a custom actor type and apply authentication middlewares. + +### Example: Allow Multiple Actor Types + +To allow multiple actor types to access an API route, pass an array of actor types to the `authenticate` middleware: + +```ts title="src/api/middlewares.ts" +import { + defineMiddlewares, + authenticate, +} from "@medusajs/framework/http" + +export default defineMiddlewares({ + routes: [ + { + matcher: "/custom*", + middlewares: [authenticate(["user", "customer"], ["session", "bearer"])], + }, + ], +}) +``` + +*** + +## Authentication Opt-Out + +To disable the authentication guard on custom routes under the `/admin` path prefix, export an `AUTHENTICATE` variable in the route file with its value set to `false`. + +For example: + +```ts title="src/api/admin/custom/route.ts" highlights={[["15"]]} +import type { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" + +export const GET = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: "Hello", + }) +} + +export const AUTHENTICATE = false +``` + +Now, any request sent to the `/admin/custom` API route is allowed, regardless if the admin user is authenticated. + +*** + +## Authenticated Request Type + +To access the authentication details in an API route, such as the logged-in user's ID, set the type of the first request parameter to `AuthenticatedMedusaRequest`. It extends `MedusaRequest`. + +The `auth_context.actor_id` property of `AuthenticatedMedusaRequest` holds the ID of the authenticated user or customer. If there isn't any authenticated user or customer, `auth_context` is `undefined`. + +If you opt-out of authentication in a route as mentioned in the [previous section](#authentication-opt-out), you can't access the authenticated user or customer anymore. Use the [authenticate middleware](#protect-custom-api-routes) instead. + +### Retrieve Logged-In Customer's Details + +You can access the logged-in customer’s ID in all API routes starting with `/store` using the `auth_context.actor_id` property of the `AuthenticatedMedusaRequest` object. + +For example: + +```ts title="src/api/store/custom/route.ts" highlights={[["19", "req.auth_context.actor_id", "Access the logged-in customer's ID."]]} collapsibleLines="1-7" expandButtonLabel="Show Imports" +import type { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { Modules } from "@medusajs/framework/utils" +import { ICustomerModuleService } from "@medusajs/framework/types" + +export const GET = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + if (req.auth_context?.actor_id) { + // retrieve customer + const customerModuleService: ICustomerModuleService = req.scope.resolve( + Modules.CUSTOMER + ) + + const customer = await customerModuleService.retrieveCustomer( + req.auth_context.actor_id + ) + } + + // ... +} +``` + +In this example, you resolve the Customer Module's main service, then use it to retrieve the logged-in customer, if available. + +### Retrieve Logged-In Admin User's Details + +You can access the logged-in admin user’s ID in all API Routes starting with `/admin` using the `auth_context.actor_id` property of the `AuthenticatedMedusaRequest` object. + +For example: + +```ts title="src/api/admin/custom/route.ts" highlights={[["17", "req.auth_context.actor_id", "Access the logged-in admin user's ID."]]} collapsibleLines="1-7" expandButtonLabel="Show Imports" +import type { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { Modules } from "@medusajs/framework/utils" +import { IUserModuleService } from "@medusajs/framework/types" + +export const GET = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const userModuleService: IUserModuleService = req.scope.resolve( + Modules.USER + ) + + const user = await userModuleService.retrieveUser( + req.auth_context.actor_id + ) + + // ... +} +``` + +In the route handler, you resolve the User Module's main service, then use it to retrieve the logged-in admin user. + + # Configure Request Body Parser In this chapter, you'll learn how to configure the request body parser for your API routes. @@ -9243,108 +9445,6 @@ export async function POST( Check out the [uploadFilesWorkflow reference](https://docs.medusajs.com/resources/references/medusa-workflows/uploadFilesWorkflow/index.html.md) for details on the expected input and output of the workflow. -# API Route Response - -In this chapter, you'll learn how to send a response in your API route. - -## Send a JSON Response - -To send a JSON response, use the `json` method of the `MedusaResponse` object passed as the second parameter of your API route handler. - -For example: - -```ts title="src/api/custom/route.ts" highlights={jsonHighlights} -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" - -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: "Hello, World!", - }) -} -``` - -This API route returns the following JSON object: - -```json -{ - "message": "Hello, World!" -} -``` - -*** - -## Set Response Status Code - -By default, setting the JSON data using the `json` method returns a response with a `200` status code. - -To change the status code, use the `status` method of the `MedusaResponse` object. - -For example: - -```ts title="src/api/custom/route.ts" highlights={statusHighlight} -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" - -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.status(201).json({ - message: "Hello, World!", - }) -} -``` - -The response of this API route has the status code `201`. - -*** - -## Change Response Content Type - -To return response data other than a JSON object, use the `writeHead` method of the `MedusaResponse` object. It allows you to set the response headers, including the content type. - -For example, to create an API route that returns an event stream: - -```ts highlights={streamHighlights} -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" - -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.writeHead(200, { - "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", - Connection: "keep-alive", - }) - - const interval = setInterval(() => { - res.write("Streaming data...\n") - }, 3000) - - req.on("end", () => { - clearInterval(interval) - res.end() - }) -} -``` - -The `writeHead` method accepts two parameters: - -1. The first one is the response's status code. -2. The second is an object of key-value pairs to set the headers of the response. - -This API route opens a stream by setting the `Content-Type` in the header to `text/event-stream`. It then simulates a stream by creating an interval that writes the stream data every three seconds. - -*** - -## Do More with Responses - -The `MedusaResponse` type is based on [Express's Response](https://expressjs.com/en/api.html#res). Refer to their API reference for other uses of responses. - - # 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. @@ -9594,206 +9694,319 @@ 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). -# Protected Routes +# API Route Response -In this chapter, you’ll learn how to create protected routes. +In this chapter, you'll learn how to send a response in your API route. -## What is a Protected Route? +## Send a JSON Response -A protected route is a route that requires requests to be user-authenticated before performing the route's functionality. Otherwise, the request fails, and the user is prevented access. - -*** - -## Default Protected Routes - -Medusa applies an authentication guard on routes starting with `/admin`, including custom API routes. - -Requests to `/admin` must be user-authenticated to access the route. - -Refer to the API Reference for [Admin](https://docs.medusajs.com/api/admin#authentication) and [Store](https://docs.medusajs.com/api/store#authentication) authentication methods. - -*** - -## Protect Custom API Routes - -To protect custom API Routes to only allow authenticated customer or admin users, use the `authenticate` middleware from the Medusa Framework. +To send a JSON response, use the `json` method of the `MedusaResponse` object passed as the second parameter of your API route handler. For example: -```ts title="src/api/middlewares.ts" highlights={highlights} -import { - defineMiddlewares, - authenticate, -} from "@medusajs/framework/http" - -export default defineMiddlewares({ - routes: [ - { - matcher: "/custom/admin*", - middlewares: [authenticate("user", ["session", "bearer", "api-key"])], - }, - { - matcher: "/custom/customer*", - middlewares: [authenticate("customer", ["session", "bearer"])], - }, - ], -}) -``` - -The `authenticate` middleware function accepts three parameters: - -1. The type of user authenticating. Use `user` for authenticating admin users, and `customer` for authenticating customers. You can also pass `*` to allow all types of users, or pass an array of actor types. -2. An array of types of authentication methods allowed. Both `user` and `customer` scopes support `session` and `bearer`. The `admin` scope also supports the `api-key` authentication method. -3. An optional object of configurations accepting the following properties: - - `allowUnauthenticated`: (default: `false`) A boolean indicating whether authentication is required. For example, you may have an API route where you want to access the logged-in customer if available, but guest customers can still access it too. - - `allowUnregistered` (default: `false`): A boolean indicating if unregistered users should be allowed access. This is useful when you want to allow users who aren’t registered to access certain routes. - -### Example: Custom Actor Type - -For example, to require authentication of a custom actor type `manager` to an API route: - -```ts title="src/api/middlewares.ts" -import { - defineMiddlewares, - authenticate, -} from "@medusajs/framework/http" - -export default defineMiddlewares({ - routes: [ - { - matcher: "/manager*", - middlewares: [authenticate("manager", ["session", "bearer"])], - }, - ], -}) -``` - -Refer to the [Custom Actor-Type Guide](https://docs.medusajs.com/resources/commerce-modules/auth/create-actor-type/index.html.md) for detailed explanation on how to create a custom actor type and apply authentication middlewares. - -### Example: Allow Multiple Actor Types - -To allow multiple actor types to access an API route, pass an array of actor types to the `authenticate` middleware: - -```ts title="src/api/middlewares.ts" -import { - defineMiddlewares, - authenticate, -} from "@medusajs/framework/http" - -export default defineMiddlewares({ - routes: [ - { - matcher: "/custom*", - middlewares: [authenticate(["user", "customer"], ["session", "bearer"])], - }, - ], -}) -``` - -*** - -## Authentication Opt-Out - -To disable the authentication guard on custom routes under the `/admin` path prefix, export an `AUTHENTICATE` variable in the route file with its value set to `false`. - -For example: - -```ts title="src/api/admin/custom/route.ts" highlights={[["15"]]} -import type { - AuthenticatedMedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" +```ts title="src/api/custom/route.ts" highlights={jsonHighlights} +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" export const GET = async ( - req: AuthenticatedMedusaRequest, + req: MedusaRequest, res: MedusaResponse ) => { res.json({ - message: "Hello", + message: "Hello, World!", }) } - -export const AUTHENTICATE = false ``` -Now, any request sent to the `/admin/custom` API route is allowed, regardless if the admin user is authenticated. +This API route returns the following JSON object: + +```json +{ + "message": "Hello, World!" +} +``` *** -## Authenticated Request Type +## Set Response Status Code -To access the authentication details in an API route, such as the logged-in user's ID, set the type of the first request parameter to `AuthenticatedMedusaRequest`. It extends `MedusaRequest`. +By default, setting the JSON data using the `json` method returns a response with a `200` status code. -The `auth_context.actor_id` property of `AuthenticatedMedusaRequest` holds the ID of the authenticated user or customer. If there isn't any authenticated user or customer, `auth_context` is `undefined`. - -If you opt-out of authentication in a route as mentioned in the [previous section](#authentication-opt-out), you can't access the authenticated user or customer anymore. Use the [authenticate middleware](#protect-custom-api-routes) instead. - -### Retrieve Logged-In Customer's Details - -You can access the logged-in customer’s ID in all API routes starting with `/store` using the `auth_context.actor_id` property of the `AuthenticatedMedusaRequest` object. +To change the status code, use the `status` method of the `MedusaResponse` object. For example: -```ts title="src/api/store/custom/route.ts" highlights={[["19", "req.auth_context.actor_id", "Access the logged-in customer's ID."]]} collapsibleLines="1-7" expandButtonLabel="Show Imports" -import type { - AuthenticatedMedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { Modules } from "@medusajs/framework/utils" -import { ICustomerModuleService } from "@medusajs/framework/types" +```ts title="src/api/custom/route.ts" highlights={statusHighlight} +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" export const GET = async ( - req: AuthenticatedMedusaRequest, + req: MedusaRequest, res: MedusaResponse ) => { - if (req.auth_context?.actor_id) { - // retrieve customer - const customerModuleService: ICustomerModuleService = req.scope.resolve( - Modules.CUSTOMER - ) + res.status(201).json({ + message: "Hello, World!", + }) +} +``` - const customer = await customerModuleService.retrieveCustomer( - req.auth_context.actor_id - ) +The response of this API route has the status code `201`. + +*** + +## Change Response Content Type + +To return response data other than a JSON object, use the `writeHead` method of the `MedusaResponse` object. It allows you to set the response headers, including the content type. + +For example, to create an API route that returns an event stream: + +```ts highlights={streamHighlights} +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" + +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }) + + const interval = setInterval(() => { + res.write("Streaming data...\n") + }, 3000) + + req.on("end", () => { + clearInterval(interval) + res.end() + }) +} +``` + +The `writeHead` method accepts two parameters: + +1. The first one is the response's status code. +2. The second is an object of key-value pairs to set the headers of the response. + +This API route opens a stream by setting the `Content-Type` in the header to `text/event-stream`. It then simulates a stream by creating an interval that writes the stream data every three seconds. + +*** + +## Do More with Responses + +The `MedusaResponse` type is based on [Express's Response](https://expressjs.com/en/api.html#res). Refer to their API reference for other uses of responses. + + +# Event Data Payload + +In this chapter, you'll learn how subscribers receive an event's data payload. + +## 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!/events-reference) 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 + } } ``` -In this example, you resolve the Customer Module's main service, then use it to retrieve the logged-in customer, if available. +### Without Service Factory -### Retrieve Logged-In Admin User's Details +```ts title="src/modules/blog/service.ts" highlights={["6"]} +import { IEventBusService } from "@medusajs/framework/types" -You can access the logged-in admin user’s ID in all API Routes starting with `/admin` using the `auth_context.actor_id` property of the `AuthenticatedMedusaRequest` object. +class BlogModuleService { + protected eventBusService_: AbstractEventBusModuleService -For example: + constructor({ event_bus }) { + this.eventBusService_ = event_bus + } +} +``` -```ts title="src/api/admin/custom/route.ts" highlights={[["17", "req.auth_context.actor_id", "Access the logged-in admin user's ID."]]} collapsibleLines="1-7" expandButtonLabel="Show Imports" -import type { - AuthenticatedMedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" +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" -import { IUserModuleService } from "@medusajs/framework/types" - -export const GET = async ( - req: AuthenticatedMedusaRequest, - res: MedusaResponse -) => { - const userModuleService: IUserModuleService = req.scope.resolve( - Modules.USER - ) - - const user = await userModuleService.retrieveUser( - req.auth_context.actor_id - ) +module.exports = defineConfig({ // ... -} + modules: [ + { + resolve: "./src/modules/blog", + dependencies: [ + Modules.EVENT_BUS, + ], + }, + ], +}) ``` -In the route handler, you resolve the User Module's main service, then use it to retrieve the logged-in admin user. +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 @@ -9868,6 +10081,376 @@ npx medusa db:migrate The first command generates the migration under the `migrations` directory of your module's directory, and the second reflects it on the database. +# Data Model Database Index + +In this chapter, you’ll learn how to define a database index on a data model. + +You can also define an index on a property as explained in the [Properties chapter](https://docs.medusajs.com/learn/fundamentals/data-models/properties#define-database-index-on-property/index.html.md). + +## Define Database Index on Data Model + +A data model has an `indexes` method that defines database indices on its properties. + +The index can be on multiple columns (composite index). For example: + +```ts highlights={dataModelIndexHighlights} +import { model } from "@medusajs/framework/utils" + +const MyCustom = model.define("my_custom", { + id: model.id().primaryKey(), + name: model.text(), + age: model.number(), +}).indexes([ + { + on: ["name", "age"], + }, +]) + +export default MyCustom +``` + +The `indexes` method receives an array of indices as a parameter. Each index is an object with a required `on` property indicating the properties to apply the index on. + +In the above example, you define a composite index on the `name` and `age` properties. + +### Index Conditions + +An index can have conditions. For example: + +```ts highlights={conditionHighlights} +import { model } from "@medusajs/framework/utils" + +const MyCustom = model.define("my_custom", { + id: model.id().primaryKey(), + name: model.text(), + age: model.number(), +}).indexes([ + { + on: ["name", "age"], + where: { + age: 30, + }, + }, +]) + +export default MyCustom +``` + +The index object passed to `indexes` accepts a `where` property whose value is an object of conditions. The object's key is a property's name, and its value is the condition on that property. + +In the example above, the composite index is created on the `name` and `age` properties when the `age`'s value is `30`. + +A property's condition can be a negation. For example: + +```ts highlights={negationHighlights} +import { model } from "@medusajs/framework/utils" + +const MyCustom = model.define("my_custom", { + id: model.id().primaryKey(), + name: model.text(), + age: model.number().nullable(), +}).indexes([ + { + on: ["name", "age"], + where: { + age: { + $ne: null, + }, + }, + }, +]) + +export default MyCustom +``` + +A property's value in `where` can be an object having a `$ne` property. `$ne`'s value indicates what the specified property's value shouldn't be. + +In the example above, the composite index is created on the `name` and `age` properties when `age`'s value is not `null`. + +### Unique Database Index + +The object passed to `indexes` accepts a `unique` property indicating that the created index must be a unique index. + +For example: + +```ts highlights={uniqueHighlights} +import { model } from "@medusajs/framework/utils" + +const MyCustom = model.define("my_custom", { + id: model.id().primaryKey(), + name: model.text(), + age: model.number(), +}).indexes([ + { + on: ["name", "age"], + unique: true, + }, +]) + +export default MyCustom +``` + +This creates a unique composite index on the `name` and `age` properties. + + +# Infer Type of Data Model + +In this chapter, you'll learn how to infer the type of a data model. + +## How to Infer Type of Data Model? + +Consider you have a `Post` data model. You can't reference this data model in a type, such as a workflow input or service method output types, since it's a variable. + +Instead, Medusa provides `InferTypeOf` that transforms your data model to a type. + +For example: + +```ts +import { InferTypeOf } from "@medusajs/framework/types" +import { Post } from "../modules/blog/models/post" // relative path to the model + +export type Post = InferTypeOf +``` + +`InferTypeOf` accepts as a type argument the type of the data model. + +Since the `Post` data model is a variable, use the `typeof` operator to pass the data model as a type argument to `InferTypeOf`. + +You can now use the `Post` type to reference a data model in other types, such as in workflow inputs or service method outputs: + +```ts title="Example Service" +// other imports... +import { InferTypeOf } from "@medusajs/framework/types" +import { Post } from "../models/post" + +type Post = InferTypeOf + +class BlogModuleService extends MedusaService({ Post }) { + async doSomething(): Promise { + // ... + } +} +``` + + +# Manage Relationships + +In this chapter, you'll learn how to manage relationships between data models when creating, updating, or retrieving records using the module's main service. + +## Manage One-to-One Relationship + +### BelongsTo Side of One-to-One + +When you create a record of a data model that belongs to another through a one-to-one relation, pass the ID of the other data model's record in the `{relation}_id` property, where `{relation}` is the name of the relation property. + +For example, assuming you have the [User and Email data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#one-to-one-relationship/index.html.md), set an email's user ID as follows: + +```ts highlights={belongsHighlights} +// when creating an email +const email = await helloModuleService.createEmails({ + // other properties... + user_id: "123", +}) + +// when updating an email +const email = await helloModuleService.updateEmails({ + id: "321", + // other properties... + user_id: "123", +}) +``` + +In the example above, you pass the `user_id` property when creating or updating an email to specify the user it belongs to. + +### HasOne Side + +When you create a record of a data model that has one of another, pass the ID of the other data model's record in the relation property. + +For example, assuming you have the [User and Email data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#one-to-one-relationship/index.html.md), set a user's email ID as follows: + +```ts highlights={hasOneHighlights} +// when creating a user +const user = await helloModuleService.createUsers({ + // other properties... + email: "123", +}) + +// when updating a user +const user = await helloModuleService.updateUsers({ + id: "321", + // other properties... + email: "123", +}) +``` + +In the example above, you pass the `email` property when creating or updating a user to specify the email it has. + +*** + +## Manage One-to-Many Relationship + +In a one-to-many relationship, you can only manage the associations from the `belongsTo` side. + +When you create a record of the data model on the `belongsTo` side, pass the ID of the other data model's record in the `{relation}_id` property, where `{relation}` is the name of the relation property. + +For example, assuming you have the [Product and Store data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#one-to-many-relationship/index.html.md), set a product's store ID as follows: + +```ts highlights={manyBelongsHighlights} +// when creating a product +const product = await helloModuleService.createProducts({ + // other properties... + store_id: "123", +}) + +// when updating a product +const product = await helloModuleService.updateProducts({ + id: "321", + // other properties... + store_id: "123", +}) +``` + +In the example above, you pass the `store_id` property when creating or updating a product to specify the store it belongs to. + +*** + +## Manage Many-to-Many Relationship + +If your many-to-many relation is represented with a [pivotEntity](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-with-custom-columns/index.html.md), refer to [this section](#manage-many-to-many-relationship-with-pivotentity) instead. + +### Create Associations + +When you create a record of a data model that has a many-to-many relationship to another data model, pass an array of IDs of the other data model's records in the relation property. + +For example, assuming you have the [Order and Product data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-relationship/index.html.md), set the association between products and orders as follows: + +```ts highlights={manyHighlights} +// when creating a product +const product = await helloModuleService.createProducts({ + // other properties... + orders: ["123", "321"], +}) + +// when creating an order +const order = await helloModuleService.createOrders({ + id: "321", + // other properties... + products: ["123", "321"], +}) +``` + +In the example above, you pass the `orders` property when you create a product, and you pass the `products` property when you create an order. + +### Update Associations + +When you use the `update` methods generated by the service factory, you also pass an array of IDs as the relation property's value to add new associated records. + +However, this removes any existing associations to records whose IDs aren't included in the array. + +For example, assuming you have the [Order and Product data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-relationship/index.html.md), you update the product's related orders as so: + +```ts +const product = await helloModuleService.updateProducts({ + id: "123", + // other properties... + orders: ["321"], +}) +``` + +If the product was associated with an order, and you don't include that order's ID in the `orders` array, the association between the product and order is removed. + +So, to add a new association without removing existing ones, retrieve the product first to pass its associated orders when updating the product: + +```ts highlights={updateAssociationHighlights} +const product = await helloModuleService.retrieveProduct( + "123", + { + relations: ["orders"], + } +) + +const updatedProduct = await helloModuleService.updateProducts({ + id: product.id, + // other properties... + orders: [ + ...product.orders.map((order) => order.id), + "321", + ], +}) +``` + +This keeps existing associations between the product and orders, and adds a new one. + +*** + +## Manage Many-to-Many Relationship with pivotEntity + +If your many-to-many relation is represented without a [pivotEntity](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-with-custom-columns/index.html.md), refer to [this section](#manage-many-to-many-relationship) instead. + +If you have a many-to-many relation with a `pivotEntity` specified, make sure to pass the data model representing the pivot table to [MedusaService](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md) that your module's service extends. + +For example, assuming you have the [Order, Product, and OrderProduct models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-with-custom-columns/index.html.md), add `OrderProduct` to `MedusaService`'s object parameter: + +```ts highlights={["4"]} +class BlogModuleService extends MedusaService({ + Order, + Product, + OrderProduct, +}) {} +``` + +This will generate Create, Read, Update and Delete (CRUD) methods for the `OrderProduct` data model, which you can use to create relations between orders and products and manage the extra columns in the pivot table. + +For example: + +```ts +// create order-product association +const orderProduct = await blogModuleService.createOrderProducts({ + order_id: "123", + product_id: "123", + metadata: { + test: true, + }, +}) + +// update order-product association +const orderProduct = await blogModuleService.updateOrderProducts({ + id: "123", + metadata: { + test: false, + }, +}) + +// delete order-product association +await blogModuleService.deleteOrderProducts("123") +``` + +Since the `OrderProduct` data model belongs to the `Order` and `Product` data models, you can set its order and product as explained in the [one-to-many relationship section](#manage-one-to-many-relationship) using `order_id` and `product_id`. + +Refer to the [service factory reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) for a full list of generated methods and their usages. + +*** + +## Retrieve Records of Relation + +The `list`, `listAndCount`, and `retrieve` methods of a module's main service accept as a second parameter an object of options. + +To retrieve the records associated with a data model's records through a relationship, pass in the second parameter object a `relations` property whose value is an array of relationship names. + +For example, assuming you have the [Order and Product data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-relationship/index.html.md), you retrieve a product's orders as follows: + +```ts highlights={retrieveHighlights} +const product = await blogModuleService.retrieveProducts( + "123", + { + relations: ["orders"], + } +) +``` + +In the example above, the retrieved product has an `orders` property, whose value is an array of orders associated with the product. + + # Data Model Properties In this chapter, you'll learn about the different property types you can use in a data model and how to configure a data model's properties. @@ -10220,336 +10803,6 @@ const posts = await blogModuleService.listPosts({ This retrieves records that include `New Products` in their `title` property. -# Manage Relationships - -In this chapter, you'll learn how to manage relationships between data models when creating, updating, or retrieving records using the module's main service. - -## Manage One-to-One Relationship - -### BelongsTo Side of One-to-One - -When you create a record of a data model that belongs to another through a one-to-one relation, pass the ID of the other data model's record in the `{relation}_id` property, where `{relation}` is the name of the relation property. - -For example, assuming you have the [User and Email data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#one-to-one-relationship/index.html.md), set an email's user ID as follows: - -```ts highlights={belongsHighlights} -// when creating an email -const email = await helloModuleService.createEmails({ - // other properties... - user_id: "123", -}) - -// when updating an email -const email = await helloModuleService.updateEmails({ - id: "321", - // other properties... - user_id: "123", -}) -``` - -In the example above, you pass the `user_id` property when creating or updating an email to specify the user it belongs to. - -### HasOne Side - -When you create a record of a data model that has one of another, pass the ID of the other data model's record in the relation property. - -For example, assuming you have the [User and Email data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#one-to-one-relationship/index.html.md), set a user's email ID as follows: - -```ts highlights={hasOneHighlights} -// when creating a user -const user = await helloModuleService.createUsers({ - // other properties... - email: "123", -}) - -// when updating a user -const user = await helloModuleService.updateUsers({ - id: "321", - // other properties... - email: "123", -}) -``` - -In the example above, you pass the `email` property when creating or updating a user to specify the email it has. - -*** - -## Manage One-to-Many Relationship - -In a one-to-many relationship, you can only manage the associations from the `belongsTo` side. - -When you create a record of the data model on the `belongsTo` side, pass the ID of the other data model's record in the `{relation}_id` property, where `{relation}` is the name of the relation property. - -For example, assuming you have the [Product and Store data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#one-to-many-relationship/index.html.md), set a product's store ID as follows: - -```ts highlights={manyBelongsHighlights} -// when creating a product -const product = await helloModuleService.createProducts({ - // other properties... - store_id: "123", -}) - -// when updating a product -const product = await helloModuleService.updateProducts({ - id: "321", - // other properties... - store_id: "123", -}) -``` - -In the example above, you pass the `store_id` property when creating or updating a product to specify the store it belongs to. - -*** - -## Manage Many-to-Many Relationship - -If your many-to-many relation is represented with a [pivotEntity](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-with-custom-columns/index.html.md), refer to [this section](#manage-many-to-many-relationship-with-pivotentity) instead. - -### Create Associations - -When you create a record of a data model that has a many-to-many relationship to another data model, pass an array of IDs of the other data model's records in the relation property. - -For example, assuming you have the [Order and Product data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-relationship/index.html.md), set the association between products and orders as follows: - -```ts highlights={manyHighlights} -// when creating a product -const product = await helloModuleService.createProducts({ - // other properties... - orders: ["123", "321"], -}) - -// when creating an order -const order = await helloModuleService.createOrders({ - id: "321", - // other properties... - products: ["123", "321"], -}) -``` - -In the example above, you pass the `orders` property when you create a product, and you pass the `products` property when you create an order. - -### Update Associations - -When you use the `update` methods generated by the service factory, you also pass an array of IDs as the relation property's value to add new associated records. - -However, this removes any existing associations to records whose IDs aren't included in the array. - -For example, assuming you have the [Order and Product data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-relationship/index.html.md), you update the product's related orders as so: - -```ts -const product = await helloModuleService.updateProducts({ - id: "123", - // other properties... - orders: ["321"], -}) -``` - -If the product was associated with an order, and you don't include that order's ID in the `orders` array, the association between the product and order is removed. - -So, to add a new association without removing existing ones, retrieve the product first to pass its associated orders when updating the product: - -```ts highlights={updateAssociationHighlights} -const product = await helloModuleService.retrieveProduct( - "123", - { - relations: ["orders"], - } -) - -const updatedProduct = await helloModuleService.updateProducts({ - id: product.id, - // other properties... - orders: [ - ...product.orders.map((order) => order.id), - "321", - ], -}) -``` - -This keeps existing associations between the product and orders, and adds a new one. - -*** - -## Manage Many-to-Many Relationship with pivotEntity - -If your many-to-many relation is represented without a [pivotEntity](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-with-custom-columns/index.html.md), refer to [this section](#manage-many-to-many-relationship) instead. - -If you have a many-to-many relation with a `pivotEntity` specified, make sure to pass the data model representing the pivot table to [MedusaService](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md) that your module's service extends. - -For example, assuming you have the [Order, Product, and OrderProduct models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-with-custom-columns/index.html.md), add `OrderProduct` to `MedusaService`'s object parameter: - -```ts highlights={["4"]} -class BlogModuleService extends MedusaService({ - Order, - Product, - OrderProduct, -}) {} -``` - -This will generate Create, Read, Update and Delete (CRUD) methods for the `OrderProduct` data model, which you can use to create relations between orders and products and manage the extra columns in the pivot table. - -For example: - -```ts -// create order-product association -const orderProduct = await blogModuleService.createOrderProducts({ - order_id: "123", - product_id: "123", - metadata: { - test: true, - }, -}) - -// update order-product association -const orderProduct = await blogModuleService.updateOrderProducts({ - id: "123", - metadata: { - test: false, - }, -}) - -// delete order-product association -await blogModuleService.deleteOrderProducts("123") -``` - -Since the `OrderProduct` data model belongs to the `Order` and `Product` data models, you can set its order and product as explained in the [one-to-many relationship section](#manage-one-to-many-relationship) using `order_id` and `product_id`. - -Refer to the [service factory reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) for a full list of generated methods and their usages. - -*** - -## Retrieve Records of Relation - -The `list`, `listAndCount`, and `retrieve` methods of a module's main service accept as a second parameter an object of options. - -To retrieve the records associated with a data model's records through a relationship, pass in the second parameter object a `relations` property whose value is an array of relationship names. - -For example, assuming you have the [Order and Product data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-relationship/index.html.md), you retrieve a product's orders as follows: - -```ts highlights={retrieveHighlights} -const product = await blogModuleService.retrieveProducts( - "123", - { - relations: ["orders"], - } -) -``` - -In the example above, the retrieved product has an `orders` property, whose value is an array of orders associated with the product. - - -# Data Model Database Index - -In this chapter, you’ll learn how to define a database index on a data model. - -You can also define an index on a property as explained in the [Properties chapter](https://docs.medusajs.com/learn/fundamentals/data-models/properties#define-database-index-on-property/index.html.md). - -## Define Database Index on Data Model - -A data model has an `indexes` method that defines database indices on its properties. - -The index can be on multiple columns (composite index). For example: - -```ts highlights={dataModelIndexHighlights} -import { model } from "@medusajs/framework/utils" - -const MyCustom = model.define("my_custom", { - id: model.id().primaryKey(), - name: model.text(), - age: model.number(), -}).indexes([ - { - on: ["name", "age"], - }, -]) - -export default MyCustom -``` - -The `indexes` method receives an array of indices as a parameter. Each index is an object with a required `on` property indicating the properties to apply the index on. - -In the above example, you define a composite index on the `name` and `age` properties. - -### Index Conditions - -An index can have conditions. For example: - -```ts highlights={conditionHighlights} -import { model } from "@medusajs/framework/utils" - -const MyCustom = model.define("my_custom", { - id: model.id().primaryKey(), - name: model.text(), - age: model.number(), -}).indexes([ - { - on: ["name", "age"], - where: { - age: 30, - }, - }, -]) - -export default MyCustom -``` - -The index object passed to `indexes` accepts a `where` property whose value is an object of conditions. The object's key is a property's name, and its value is the condition on that property. - -In the example above, the composite index is created on the `name` and `age` properties when the `age`'s value is `30`. - -A property's condition can be a negation. For example: - -```ts highlights={negationHighlights} -import { model } from "@medusajs/framework/utils" - -const MyCustom = model.define("my_custom", { - id: model.id().primaryKey(), - name: model.text(), - age: model.number().nullable(), -}).indexes([ - { - on: ["name", "age"], - where: { - age: { - $ne: null, - }, - }, - }, -]) - -export default MyCustom -``` - -A property's value in `where` can be an object having a `$ne` property. `$ne`'s value indicates what the specified property's value shouldn't be. - -In the example above, the composite index is created on the `name` and `age` properties when `age`'s value is not `null`. - -### Unique Database Index - -The object passed to `indexes` accepts a `unique` property indicating that the created index must be a unique index. - -For example: - -```ts highlights={uniqueHighlights} -import { model } from "@medusajs/framework/utils" - -const MyCustom = model.define("my_custom", { - id: model.id().primaryKey(), - name: model.text(), - age: model.number(), -}).indexes([ - { - on: ["name", "age"], - unique: true, - }, -]) - -export default MyCustom -``` - -This creates a unique composite index on the `name` and `age` properties. - - # Data Model Relationships In this chapter, you’ll learn how to define relationships between data models in your module. @@ -10943,219 +11196,6 @@ 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). -# 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!/events-reference) for a full list of events emitted by Medusa and their data payloads. */} - - -# Emit Workflow and Service Events - -In this chapter, you'll learn about event types and how to emit an event in a service or workflow. - -## Event Types - -In your customization, you can emit an event, then listen to it in a subscriber and perform an asynchronus action, such as send a notification or data to a third-party system. - -There are two types of events in Medusa: - -1. Workflow event: an event that's emitted in a workflow after a commerce feature is performed. For example, Medusa emits the `order.placed` event after a cart is completed. -2. Service event: an event that's emitted to track, trace, or debug processes under the hood. For example, you can emit an event with an audit trail. - -### Which Event Type to Use? - -**Workflow events** are the most common event type in development, as most custom features and customizations are built around workflows. - -Some examples of workflow events: - -1. When a user creates a blog post and you're emitting an event to send a newsletter email. -2. When you finish syncing products to a third-party system and you want to notify the admin user of new products added. -3. When a customer purchases a digital product and you want to generate and send it to them. - -You should only go for a **service event** if you're emitting an event for processes under the hood that don't directly affect front-facing features. - -Some examples of service events: - -1. When you're tracing data manipulation and changes, and you want to track every time some custom data is changed. -2. When you're syncing data with a search engine. - -*** - -## Emit Event in a Workflow - -To emit a workflow event, use the `emitEventStep` helper step provided in the `@medusajs/medusa/core-flows` package. - -For example: - -```ts highlights={highlights} -import { - createWorkflow, -} from "@medusajs/framework/workflows-sdk" -import { - emitEventStep, -} from "@medusajs/medusa/core-flows" - -const helloWorldWorkflow = createWorkflow( - "hello-world", - () => { - // ... - - emitEventStep({ - eventName: "custom.created", - data: { - id: "123", - // other data payload - }, - }) - } -) -``` - -The `emitEventStep` accepts an object having the following properties: - -- `eventName`: The event's name. -- `data`: The data payload as an object. You can pass any properties in the object, and subscribers listening to the event will receive this data in the event's payload. - -In this example, you emit the event `custom.created` and pass in the data payload an ID property. - -### Test it Out - -If you execute the workflow, the event is emitted and you can see it in your application's logs. - -Any subscribers listening to the event are executed. - -*** - -## Emit Event in a Service - -To emit a service event: - -1. Resolve `event_bus` from the module's container in your service's constructor: - -### Extending Service Factory - -```ts title="src/modules/blog/service.ts" highlights={["9"]} -import { IEventBusService } from "@medusajs/framework/types" -import { MedusaService } from "@medusajs/framework/utils" - -class BlogModuleService extends MedusaService({ - Post, -}){ - protected eventBusService_: AbstractEventBusModuleService - - constructor({ event_bus }) { - super(...arguments) - this.eventBusService_ = event_bus - } -} -``` - -### Without Service Factory - -```ts title="src/modules/blog/service.ts" highlights={["6"]} -import { IEventBusService } from "@medusajs/framework/types" - -class BlogModuleService { - protected eventBusService_: AbstractEventBusModuleService - - constructor({ event_bus }) { - this.eventBusService_ = event_bus - } -} -``` - -2. Use the event bus service's `emit` method in the service's methods to emit an event: - -```ts title="src/modules/blog/service.ts" highlights={serviceHighlights} -class BlogModuleService { - // ... - performAction() { - // TODO perform action - - this.eventBusService_.emit({ - name: "custom.event", - data: { - id: "123", - // other data payload - }, - }) - } -} -``` - -The method accepts an object having the following properties: - -- `name`: The event's name. -- `data`: The data payload as an object. You can pass any properties in the object, and subscribers listening to the event will receive this data in the event's payload. - -3. By default, the Event Module's service isn't injected into your module's container. To add it to the container, pass it in the module's registration object in `medusa-config.ts` in the `dependencies` property: - -```ts title="medusa-config.ts" highlights={depsHighlight} -import { Modules } from "@medusajs/framework/utils" - -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "./src/modules/blog", - dependencies: [ - Modules.EVENT_BUS, - ], - }, - ], -}) -``` - -The `dependencies` property accepts an array of module registration keys. The specified modules' main services are injected into the module's container. - -That's how you can resolve it in your module's main service's constructor. - -### Test it Out - -If you execute the `performAction` method of your service, the event is emitted and you can see it in your application's logs. - -Any subscribers listening to the event are also executed. - - # Add 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. @@ -11375,210 +11415,6 @@ export default defineLink( ``` -# Link - -In this chapter, you’ll learn what Link is and how to use it to manage links. - -As of [Medusa v2.2.0](https://github.com/medusajs/medusa/releases/tag/v2.2.0), Remote Link has been deprecated in favor of Link. They have the same usage, so you only need to change the key used to resolve the tool from the Medusa container as explained below. - -## What is Link? - -Link is a class with utility methods to manage links between data models. It’s registered in the Medusa container under the `link` registration name. - -For example: - -```ts collapsibleLines="1-9" expandButtonLabel="Show Imports" -import { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { - ContainerRegistrationKeys, -} from "@medusajs/framework/utils" - -export async function POST( - req: MedusaRequest, - res: MedusaResponse -): Promise { - const link = req.scope.resolve( - ContainerRegistrationKeys.LINK - ) - - // ... -} -``` - -You can use its methods to manage links, such as create or delete links. - -*** - -## Create Link - -To create a link between records of two data models, use the `create` method of Link. - -For example: - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - "helloModuleService": { - my_custom_id: "mc_123", - }, -}) -``` - -The `create` method accepts as a parameter an object. The object’s keys are the names of the linked modules. - -The keys (names of linked modules) must be in the same [direction](https://docs.medusajs.com/learn/fundamentals/module-links/directions/index.html.md) of the link definition. - -The value of each module’s property is an object, whose keys are of the format `{data_model_snake_name}_id`, and values are the IDs of the linked record. - -So, in the example above, you link a record of the `MyCustom` data model in a `hello` module to a `Product` record in the Product Module. - -### Enforced Integrity Constraints on Link Creation - -Medusa enforces integrity constraints on links based on the link's relation type. So, an error is thrown in the following scenarios: - -- If the link is one-to-one and one of the linked records already has a link to another record of the same data model. For example: - -```ts -// no error -await link.create({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - "helloModuleService": { - my_custom_id: "mc_123", - }, -}) - -// throws an error because `prod_123` already has a link to `mc_123` -await link.create({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - "helloModuleService": { - my_custom_id: "mc_456", - }, -}) -``` - -- If the link is one-to-many and the "one" side already has a link to another record of the same data model. For example, if a product can have many `MyCustom` records, but a `MyCustom` record can only have one product: - -```ts -// no error -await link.create({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - "helloModuleService": { - my_custom_id: "mc_123", - }, -}) - -// also no error -await link.create({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - "helloModuleService": { - my_custom_id: "mc_456", - }, -}) - -// throws an error because `mc_123` already has a link to `prod_123` -await link.create({ - [Modules.PRODUCT]: { - product_id: "prod_456", - }, - "helloModuleService": { - my_custom_id: "mc_123", - }, -}) -``` - -There are no integrity constraints in a many-to-many link, so you can create multiple links between the same records. - -*** - -## Dismiss Link - -To remove a link between records of two data models, use the `dismiss` method of Link. - -For example: - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.dismiss({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - "helloModuleService": { - my_custom_id: "mc_123", - }, -}) -``` - -The `dismiss` method accepts the same parameter type as the [create method](#create-link). - -The keys (names of linked modules) must be in the same [direction](https://docs.medusajs.com/learn/fundamentals/module-links/directions/index.html.md) of the link definition. - -*** - -## Cascade Delete Linked Records - -If a record is deleted, use the `delete` method of Link to delete all linked records. - -For example: - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await productModuleService.deleteVariants([variant.id]) - -await link.delete({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, -}) -``` - -This deletes all records linked to the deleted product. - -*** - -## Restore Linked Records - -If a record that was previously soft-deleted is now restored, use the `restore` method of Link to restore all linked records. - -For example: - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await productModuleService.restoreProducts(["prod_123"]) - -await link.restore({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, -}) -``` - - # Query In this chapter, you’ll learn about Query and how to use it to fetch data from modules. @@ -12130,45 +11966,680 @@ Try passing one of the Query configuration parameters, like `fields` or `limit`, Learn more about [specifing fields and relations](https://docs.medusajs.com/api/store#select-fields-and-relations) and [pagination](https://docs.medusajs.com/api/store#pagination) in the API reference. -# Infer Type of Data Model +# Link -In this chapter, you'll learn how to infer the type of a data model. +In this chapter, you’ll learn what Link is and how to use it to manage links. -## How to Infer Type of Data Model? +As of [Medusa v2.2.0](https://github.com/medusajs/medusa/releases/tag/v2.2.0), Remote Link has been deprecated in favor of Link. They have the same usage, so you only need to change the key used to resolve the tool from the Medusa container as explained below. -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. +## What is Link? -Instead, Medusa provides `InferTypeOf` that transforms your data model to a type. +Link is a class with utility methods to manage links between data models. It’s registered in the Medusa container under the `link` registration name. + +For example: + +```ts collapsibleLines="1-9" expandButtonLabel="Show Imports" +import { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { + ContainerRegistrationKeys, +} from "@medusajs/framework/utils" + +export async function POST( + req: MedusaRequest, + res: MedusaResponse +): Promise { + const link = req.scope.resolve( + ContainerRegistrationKeys.LINK + ) + + // ... +} +``` + +You can use its methods to manage links, such as create or delete links. + +*** + +## Create Link + +To create a link between records of two data models, use the `create` method of Link. For example: ```ts -import { InferTypeOf } from "@medusajs/framework/types" -import { Post } from "../modules/blog/models/post" // relative path to the model +import { Modules } from "@medusajs/framework/utils" -export type Post = InferTypeOf +// ... + +await link.create({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + "helloModuleService": { + my_custom_id: "mc_123", + }, +}) ``` -`InferTypeOf` accepts as a type argument the type of the data model. +The `create` method accepts as a parameter an object. The object’s keys are the names of the linked modules. -Since the `Post` data model is a variable, use the `typeof` operator to pass the data model as a type argument to `InferTypeOf`. +The keys (names of linked modules) must be in the same [direction](https://docs.medusajs.com/learn/fundamentals/module-links/directions/index.html.md) of the link definition. -You can now use the `Post` type to reference a data model in other types, such as in workflow inputs or service method outputs: +The value of each module’s property is an object, whose keys are of the format `{data_model_snake_name}_id`, and values are the IDs of the linked record. -```ts title="Example Service" -// other imports... -import { InferTypeOf } from "@medusajs/framework/types" -import { Post } from "../models/post" +So, in the example above, you link a record of the `MyCustom` data model in a `hello` module to a `Product` record in the Product Module. -type Post = InferTypeOf +### Enforced Integrity Constraints on Link Creation -class BlogModuleService extends MedusaService({ Post }) { - async doSomething(): Promise { - // ... +Medusa enforces integrity constraints on links based on the link's relation type. So, an error is thrown in the following scenarios: + +- If the link is one-to-one and one of the linked records already has a link to another record of the same data model. For example: + +```ts +// no error +await link.create({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + "helloModuleService": { + my_custom_id: "mc_123", + }, +}) + +// throws an error because `prod_123` already has a link to `mc_123` +await link.create({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + "helloModuleService": { + my_custom_id: "mc_456", + }, +}) +``` + +- If the link is one-to-many and the "one" side already has a link to another record of the same data model. For example, if a product can have many `MyCustom` records, but a `MyCustom` record can only have one product: + +```ts +// no error +await link.create({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + "helloModuleService": { + my_custom_id: "mc_123", + }, +}) + +// also no error +await link.create({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + "helloModuleService": { + my_custom_id: "mc_456", + }, +}) + +// throws an error because `mc_123` already has a link to `prod_123` +await link.create({ + [Modules.PRODUCT]: { + product_id: "prod_456", + }, + "helloModuleService": { + my_custom_id: "mc_123", + }, +}) +``` + +There are no integrity constraints in a many-to-many link, so you can create multiple links between the same records. + +*** + +## Dismiss Link + +To remove a link between records of two data models, use the `dismiss` method of Link. + +For example: + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.dismiss({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + "helloModuleService": { + my_custom_id: "mc_123", + }, +}) +``` + +The `dismiss` method accepts the same parameter type as the [create method](#create-link). + +The keys (names of linked modules) must be in the same [direction](https://docs.medusajs.com/learn/fundamentals/module-links/directions/index.html.md) of the link definition. + +*** + +## Cascade Delete Linked Records + +If a record is deleted, use the `delete` method of Link to delete all linked records. + +For example: + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await productModuleService.deleteVariants([variant.id]) + +await link.delete({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, +}) +``` + +This deletes all records linked to the deleted product. + +*** + +## Restore Linked Records + +If a record that was previously soft-deleted is now restored, use the `restore` method of Link to restore all linked records. + +For example: + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await productModuleService.restoreProducts(["prod_123"]) + +await link.restore({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, +}) +``` + + +# Read-Only Module Link + +In this chapter, you’ll learn what a read-only module link is and how to define one. + +## What is a Read-Only Module Link? + +Consider a scenario where you need to access related records from another module, but don't want the overhead of managing or storing the links between them. This can include cases where you're working with external data models not stored in your Medusa database, such as third-party systems. + +In those cases, instead of defining a [Module Link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) whose linked records must be stored in a link table in the database, you can use a read-only module link. A read-only module link builds a virtual relation from one data model to another in a different module without creating a link table in the database. Instead, the linked record's ID is stored in the first data model's field. + +For example, Medusa creates a read-only module link from the `Cart` data model of the [Cart Module](https://docs.medusajs.com/resources/commerce-modules/cart/index.html.md) to the `Customer` data model of the [Customer Module](https://docs.medusajs.com/resources/commerce-modules/customer/index.html.md). This link allows you to access the details of the cart's customer without managing the link. Instead, the customer's ID is stored in the `Cart` data model. + +![Diagram illustrating the read-only module link from cart to customer](https://res.cloudinary.com/dza7lstvk/image/upload/v1742212508/Medusa%20Book/cart-customer_w6vk59.jpg) + +*** + +## How to Define a Read-Only Module Link + +The `defineLink` function accepts an optional third-parameter object that can hold additional configurations for the module link. + +If you're not familiar with the `defineLink` function, refer to the [Module Links chapter](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) for more information. + +To make the module link read-only, pass the `readOnly` property as `true`. You must also set in the link configuration of the first data model a `field` property that specifies the data model's field where the linked record's ID is stored. + +For example: + +```ts highlights={highlights} +import BlogModule from "../modules/blog" +import ProductModule from "@medusajs/medusa/product" +import { defineLink } from "@medusajs/framework/utils" + +export default defineLink( + { + linkable: BlogModule.linkable.post, + field: "product_id", + }, + ProductModule.linkable.product, + { + readOnly: true, + } +) +``` + +In this example, you define a read-only module link from the Blog Module's `post` data model to the Product Module's `product` data model. You do that by: + +- Passing an object as a first parameter that accepts the linkable configuration and the field where the linked record's ID is stored. +- Setting the `readOnly` property to `true` in the third parameter. + +Unlike the stored module link, Medusa will not create a table in the database for this link. Instead, Medusa uses the ID stored in the specified field of the first data model to retrieve the linked record. + +*** + +## Retrieve Read-Only Linked Record + +[Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md) allows you to retrieve records linked through a read-only module link. + +For example, assuming you have the module link created in [the above section](#how-to-define-a-read-only-module-link), you can retrieve a post and its linked product as follows: + +```ts +const { result } = await query.graph({ + entity: "post", + fields: ["id", "product.*"], + filters: { + id: "post_123", + }, +}) +``` + +In the above example, you retrieve a post and its linked product. Medusa will use the ID of the product in the post's `product_id` field to determine which product should be retrieved. + +*** + +## Read-Only Module Link Direction + +A read-only module is uni-directional. So, you can only retrieve the linked record from the first data model. If you need to access the linked record from the second data model, you must define another read-only module link in the opposite direction. + +In the `blog` -> `product` example, you can access a post's product, but you can't access a product's posts. You would have to define another read-only module link from `product` to `blog` to access a product's posts. + +*** + +## Inverse Read-Only Module Link + +An inverse read-only module link is a read-only module link that allows you to access the linked record based on the ID stored in the second data model. + +For example, consider you want to access a product's posts. You can define a read-only module link from the Product Module's `product` data model to the Blog Module's `post` data model: + +```ts +import BlogModule from "../modules/blog" +import ProductModule from "@medusajs/medusa/product" +import { defineLink } from "@medusajs/framework/utils" + +export default defineLink( + { + linkable: ProductModule.linkable.product, + field: "id", + }, + { + ...BlogModule.linkable.post.id, + primaryKey: "product_id", + }, + { + readOnly: true, + } +) +``` + +In the above example, you define a read-only module link from the Product Module's `product` data model to the Blog Module's `post` data model. This link allows you to access a product's posts. + +Since you can't add a `post_id` field to the `product` data model, you must: + +1. Set the `field` property in the first data model's link configuration to the product's ID field. +2. Spread the `BlogModule.linkable.post.id` object in the second parameter object and set the `primaryKey` property to the field in the `post` data model that holds the product's ID. + +You can now retrieve a product and its linked posts: + +```ts +const { result } = await query.graph({ + entity: "product", + fields: ["id", "post.*"], + filters: { + id: "prod_123", + }, +}) +``` + +*** + +## One-to-One or One-to-Many? + +When you retrieve the linked record through a read-only module link, the retrieved data may be an object (one-to-one) or an array of objects (one-to-many) based on different criteria. + +|Scenario|Relation Type| +|---|---|---| +|The first data model's |One-to-one relation| +|The first data model's |One-to-many relation| +|The read-only module link is inversed.|One-to-many relation if multiple records in the second data model have the same ID of the first data model. Otherwise, one-to-one relation.| + +### One-to-One Relation + +Consider the first read-only module link you defined in this chapter: + +```ts +import BlogModule from "../modules/blog" +import ProductModule from "@medusajs/medusa/product" + +export default defineLink( + { + linkable: BlogModule.linkable.post, + field: "product_id", + }, + ProductModule.linkable.product, + { + readOnly: true, + } +) +``` + +Since the `product_id` field of a post stores the ID of a single product, the link is a one-to-one relation. When querying a post, you'll get a single product object: + +```json title="Example Data" +[ + { + "id": "post_123", + "product_id": "prod_123", + "product": { + "id": "prod_123", + // ... + } + } +] +``` + +### One-to-Many Relation + +Consider the read-only module link from the `post` data model uses an array of product IDs: + +```ts +import BlogModule from "../modules/blog" +import ProductModule from "@medusajs/medusa/product" + +export default defineLink( + { + linkable: BlogModule.linkable.post, + field: "product_ids", + }, + ProductModule.linkable.product, + { + readOnly: true, + } +) +``` + +Where `product_ids` in the `post` data model is an array of strings. In this case, the link would be a one-to-many relation. So, an array of products would be returned when querying a post: + +```json title="Example Data" +[ + { + "id": "post_123", + "product_ids": ["prod_123", "prod_124"], + "product": [ + { + "id": "prod_123", + // ... + }, + { + "id": "prod_124", + // ... + } + ] + } +] +``` + +### Relation with Inversed Read-Only Link + +If you define an inversed read-only module link where the ID of the linked record is stored in the second data model, the link can be either one-to-one or one-to-many based on the number of records in the second data model that have the same ID of the first data model. + +For example, consider the `product` -> `post` link you defined in an earlier section: + +```ts +import BlogModule from "../modules/blog" +import ProductModule from "@medusajs/medusa/product" +import { defineLink } from "@medusajs/framework/utils" + +export default defineLink( + { + linkable: ProductModule.linkable.product, + field: "id", + }, + { + ...BlogModule.linkable.post.id, + primaryKey: "product_id", + }, + { + readOnly: true, + } +) +``` + +In the above snippet, the ID of the product is stored in the `post`'s `product_id` string field. + +When you retrieve the post of a product, it may be a post object, or an array of post objects if multiple posts are linked to the product: + +```json title="Example Data" +[ + { + "id": "prod_123", + "post": { + "id": "post_123", + "product_id": "prod_123" + // ... + } + }, + { + "id": "prod_321", + "post": [ + { + "id": "post_123", + "product_id": "prod_321" + // ... + }, + { + "id": "post_124", + "product_id": "prod_321" + // ... + } + ] + } +] +``` + +If, however, you use an array field in `post`, the relation would always be one-to-many: + +```json title="Example Data" +[ + { + "id": "prod_123", + "post": [ + { + "id": "post_123", + "product_id": "prod_123" + // ... + } + ] + } +] +``` + +#### Force One-to-Many Relation + +Alternatively, you can force a one-to-many relation by setting `isList` to `true` in the first data model's link configuration. For example: + +```ts +import BlogModule from "../modules/blog" +import ProductModule from "@medusajs/medusa/product" +import { defineLink } from "@medusajs/framework/utils" + +export default defineLink( + { + linkable: ProductModule.linkable.product, + field: "id", + isList: true, + }, + { + ...BlogModule.linkable.post.id, + primaryKey: "product_id", + }, + { + readOnly: true, + } +) +``` + +In this case, the relation would always be one-to-many, even if only one post is linked to a product: + +```json title="Example Data" +[ + { + "id": "prod_123", + "post": [ + { + "id": "post_123", + "product_id": "prod_123" + // ... + } + ] + } +] +``` + +*** + +## Example: Read-Only Module Link for Virtual Data Models + +Read-only module links are most useful when working with data models that aren't stored in your Medusa database. For example, data that is stored in a third-party system. In those cases, you can define a read-only module link between a data model in Medusa and the data model in the external system, facilitating the retrieval of the linked data. + +To define the read-only module link to a virtual data model, you must: + +1. Create a `list` method in the custom module's service. This method retrieves the linked records filtered by the ID(s) of the first data model. +2. Define the read-only module link from the first data model to the virtual data model. +3. Use Query to retrieve the first data model and its linked records from the virtual data model. + +For example, consider you have a third-party Content-Management System (CMS) that you're integrating with Medusa, and you want to retrieve the posts in the CMS associated with a product in Medusa. + +To do that, first, create a CMS Module having the following service: + +Refer to the [Modules chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) to learn how to create a module and its service. + +```ts title="src/modules/cms/service.ts" +type CmsModuleOptions = { + apiKey: string +} + +export default class CmsModuleService { + private client + + constructor({}, options: CmsModuleOptions) { + this.client = new Client(options) + } + + async list( + filter: { + id: string | string[] + } + ) { + return this.client.getPosts(filter) + /** + - Example of returned data: + - + - [ + - { + - "id": "post_123", + - "product_id": "prod_321" + - }, + - { + - "id": "post_456", + - "product_id": "prod_654" + - } + - ] + */ } } ``` +The above service initializes a client, assuming your CMS has an SDK that allows you to retrieve posts. + +The service must have a `list` method to be part of the read-only module link. This method accepts the ID(s) of the products to retrieve their associated posts. The posts must include the product's ID in a field, such as `product_id`. + +Next, define a read-only module link from the Product Module to the CMS Module: + +```ts title="src/links/product-cms.ts" +import { defineLink } from "@medusajs/framework/utils" +import ProductModule from "@medusajs/medusa/product" +import { CMS_MODULE } from "../modules/cms" + +export default defineLink( + { + linkable: ProductModule.linkable.product, + field: "id", + }, + { + linkable: { + serviceName: CMS_MODULE, + alias: "cms_post", + primaryKey: "product_id", + }, + }, + { + readOnly: true, + } +) +``` + +To define the read-only module link, you must pass to `defineLink`: + +1. The first parameter: an object with the linkable configuration of the data model in Medusa, and the fields that will be passed as a filter to the CMS service. For example, if you want to filter by product title instead, you can pass `title` instead of `id`. +2. The second parameter: an object with the linkable configuration of the virtual data model in the CMS. This object must have the following properties: + - `serviceName`: The name of the service, which is the CMS Module's name. Medusa uses this name to resolve the module's service from the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md). + - `alias`: The alias to use when querying the linked records. You'll see how that works in a bit. + - `primaryKey`: The field in the CMS data model that holds the ID of a product. +3. The third parameter: an object with the `readOnly` property set to `true`. + +Now, you can use Query to retrieve a product and its linked post from the CMS: + +```ts +const { data } = await query.graph({ + entity: "product", + fields: ["id", "cms_post.*"], +}) +``` + +In the above example, each product that has a CMS post with the `product_id` field set to the product's ID will be retrieved: + +```json title="Example Data" +[ + { + "id": "prod_123", + "cms_post": { + "id": "post_123", + "product_id": "prod_123", + // ... + } + } +] +``` + +If multiple posts have their `product_id` set to a product's ID, an array of posts is returned instead: + +```json title="Example Data" +[ + { + "id": "prod_123", + "cms_post": [ + { + "id": "post_123", + "product_id": "prod_123", + // ... + }, + { + "id": "post_124", + "product_id": "prod_123", + // ... + } + ] + } +] +``` + +[Sanity Integration Tutorial](https://docs.medusajs.com/resources/integrations/guides/sanity/index.html.md). + # Query Context @@ -12830,507 +13301,6 @@ npm publish This will publish an updated version of your plugin under a new version. -# Read-Only Module Link - -In this chapter, you’ll learn what a read-only module link is and how to define one. - -## What is a Read-Only Module Link? - -Consider a scenario where you need to access related records from another module, but don't want the overhead of managing or storing the links between them. This can include cases where you're working with external data models not stored in your Medusa database, such as third-party systems. - -In those cases, instead of defining a [Module Link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) whose linked records must be stored in a link table in the database, you can use a read-only module link. A read-only module link builds a virtual relation from one data model to another in a different module without creating a link table in the database. Instead, the linked record's ID is stored in the first data model's field. - -For example, Medusa creates a read-only module link from the `Cart` data model of the [Cart Module](https://docs.medusajs.com/resources/commerce-modules/cart/index.html.md) to the `Customer` data model of the [Customer Module](https://docs.medusajs.com/resources/commerce-modules/customer/index.html.md). This link allows you to access the details of the cart's customer without managing the link. Instead, the customer's ID is stored in the `Cart` data model. - -![Diagram illustrating the read-only module link from cart to customer](https://res.cloudinary.com/dza7lstvk/image/upload/v1742212508/Medusa%20Book/cart-customer_w6vk59.jpg) - -*** - -## How to Define a Read-Only Module Link - -The `defineLink` function accepts an optional third-parameter object that can hold additional configurations for the module link. - -If you're not familiar with the `defineLink` function, refer to the [Module Links chapter](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) for more information. - -To make the module link read-only, pass the `readOnly` property as `true`. You must also set in the link configuration of the first data model a `field` property that specifies the data model's field where the linked record's ID is stored. - -For example: - -```ts highlights={highlights} -import BlogModule from "../modules/blog" -import ProductModule from "@medusajs/medusa/product" -import { defineLink } from "@medusajs/framework/utils" - -export default defineLink( - { - linkable: BlogModule.linkable.post, - field: "product_id", - }, - ProductModule.linkable.product, - { - readOnly: true, - } -) -``` - -In this example, you define a read-only module link from the Blog Module's `post` data model to the Product Module's `product` data model. You do that by: - -- Passing an object as a first parameter that accepts the linkable configuration and the field where the linked record's ID is stored. -- Setting the `readOnly` property to `true` in the third parameter. - -Unlike the stored module link, Medusa will not create a table in the database for this link. Instead, Medusa uses the ID stored in the specified field of the first data model to retrieve the linked record. - -*** - -## Retrieve Read-Only Linked Record - -[Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md) allows you to retrieve records linked through a read-only module link. - -For example, assuming you have the module link created in [the above section](#how-to-define-a-read-only-module-link), you can retrieve a post and its linked product as follows: - -```ts -const { result } = await query.graph({ - entity: "post", - fields: ["id", "product.*"], - filters: { - id: "post_123", - }, -}) -``` - -In the above example, you retrieve a post and its linked product. Medusa will use the ID of the product in the post's `product_id` field to determine which product should be retrieved. - -*** - -## Read-Only Module Link Direction - -A read-only module is uni-directional. So, you can only retrieve the linked record from the first data model. If you need to access the linked record from the second data model, you must define another read-only module link in the opposite direction. - -In the `blog` -> `product` example, you can access a post's product, but you can't access a product's posts. You would have to define another read-only module link from `product` to `blog` to access a product's posts. - -*** - -## Inverse Read-Only Module Link - -An inverse read-only module link is a read-only module link that allows you to access the linked record based on the ID stored in the second data model. - -For example, consider you want to access a product's posts. You can define a read-only module link from the Product Module's `product` data model to the Blog Module's `post` data model: - -```ts -import BlogModule from "../modules/blog" -import ProductModule from "@medusajs/medusa/product" -import { defineLink } from "@medusajs/framework/utils" - -export default defineLink( - { - linkable: ProductModule.linkable.product, - field: "id", - }, - { - ...BlogModule.linkable.post.id, - primaryKey: "product_id", - }, - { - readOnly: true, - } -) -``` - -In the above example, you define a read-only module link from the Product Module's `product` data model to the Blog Module's `post` data model. This link allows you to access a product's posts. - -Since you can't add a `post_id` field to the `product` data model, you must: - -1. Set the `field` property in the first data model's link configuration to the product's ID field. -2. Spread the `BlogModule.linkable.post.id` object in the second parameter object and set the `primaryKey` property to the field in the `post` data model that holds the product's ID. - -You can now retrieve a product and its linked posts: - -```ts -const { result } = await query.graph({ - entity: "product", - fields: ["id", "post.*"], - filters: { - id: "prod_123", - }, -}) -``` - -*** - -## One-to-One or One-to-Many? - -When you retrieve the linked record through a read-only module link, the retrieved data may be an object (one-to-one) or an array of objects (one-to-many) based on different criteria. - -|Scenario|Relation Type| -|---|---|---| -|The first data model's |One-to-one relation| -|The first data model's |One-to-many relation| -|The read-only module link is inversed.|One-to-many relation if multiple records in the second data model have the same ID of the first data model. Otherwise, one-to-one relation.| - -### One-to-One Relation - -Consider the first read-only module link you defined in this chapter: - -```ts -import BlogModule from "../modules/blog" -import ProductModule from "@medusajs/medusa/product" - -export default defineLink( - { - linkable: BlogModule.linkable.post, - field: "product_id", - }, - ProductModule.linkable.product, - { - readOnly: true, - } -) -``` - -Since the `product_id` field of a post stores the ID of a single product, the link is a one-to-one relation. When querying a post, you'll get a single product object: - -```json title="Example Data" -[ - { - "id": "post_123", - "product_id": "prod_123", - "product": { - "id": "prod_123", - // ... - } - } -] -``` - -### One-to-Many Relation - -Consider the read-only module link from the `post` data model uses an array of product IDs: - -```ts -import BlogModule from "../modules/blog" -import ProductModule from "@medusajs/medusa/product" - -export default defineLink( - { - linkable: BlogModule.linkable.post, - field: "product_ids", - }, - ProductModule.linkable.product, - { - readOnly: true, - } -) -``` - -Where `product_ids` in the `post` data model is an array of strings. In this case, the link would be a one-to-many relation. So, an array of products would be returned when querying a post: - -```json title="Example Data" -[ - { - "id": "post_123", - "product_ids": ["prod_123", "prod_124"], - "product": [ - { - "id": "prod_123", - // ... - }, - { - "id": "prod_124", - // ... - } - ] - } -] -``` - -### Relation with Inversed Read-Only Link - -If you define an inversed read-only module link where the ID of the linked record is stored in the second data model, the link can be either one-to-one or one-to-many based on the number of records in the second data model that have the same ID of the first data model. - -For example, consider the `product` -> `post` link you defined in an earlier section: - -```ts -import BlogModule from "../modules/blog" -import ProductModule from "@medusajs/medusa/product" -import { defineLink } from "@medusajs/framework/utils" - -export default defineLink( - { - linkable: ProductModule.linkable.product, - field: "id", - }, - { - ...BlogModule.linkable.post.id, - primaryKey: "product_id", - }, - { - readOnly: true, - } -) -``` - -In the above snippet, the ID of the product is stored in the `post`'s `product_id` string field. - -When you retrieve the post of a product, it may be a post object, or an array of post objects if multiple posts are linked to the product: - -```json title="Example Data" -[ - { - "id": "prod_123", - "post": { - "id": "post_123", - "product_id": "prod_123" - // ... - } - }, - { - "id": "prod_321", - "post": [ - { - "id": "post_123", - "product_id": "prod_321" - // ... - }, - { - "id": "post_124", - "product_id": "prod_321" - // ... - } - ] - } -] -``` - -If, however, you use an array field in `post`, the relation would always be one-to-many: - -```json title="Example Data" -[ - { - "id": "prod_123", - "post": [ - { - "id": "post_123", - "product_id": "prod_123" - // ... - } - ] - } -] -``` - -#### Force One-to-Many Relation - -Alternatively, you can force a one-to-many relation by setting `isList` to `true` in the first data model's link configuration. For example: - -```ts -import BlogModule from "../modules/blog" -import ProductModule from "@medusajs/medusa/product" -import { defineLink } from "@medusajs/framework/utils" - -export default defineLink( - { - linkable: ProductModule.linkable.product, - field: "id", - isList: true, - }, - { - ...BlogModule.linkable.post.id, - primaryKey: "product_id", - }, - { - readOnly: true, - } -) -``` - -In this case, the relation would always be one-to-many, even if only one post is linked to a product: - -```json title="Example Data" -[ - { - "id": "prod_123", - "post": [ - { - "id": "post_123", - "product_id": "prod_123" - // ... - } - ] - } -] -``` - -*** - -## Example: Read-Only Module Link for Virtual Data Models - -Read-only module links are most useful when working with data models that aren't stored in your Medusa database. For example, data that is stored in a third-party system. In those cases, you can define a read-only module link between a data model in Medusa and the data model in the external system, facilitating the retrieval of the linked data. - -To define the read-only module link to a virtual data model, you must: - -1. Create a `list` method in the custom module's service. This method retrieves the linked records filtered by the ID(s) of the first data model. -2. Define the read-only module link from the first data model to the virtual data model. -3. Use Query to retrieve the first data model and its linked records from the virtual data model. - -For example, consider you have a third-party Content-Management System (CMS) that you're integrating with Medusa, and you want to retrieve the posts in the CMS associated with a product in Medusa. - -To do that, first, create a CMS Module having the following service: - -Refer to the [Modules chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) to learn how to create a module and its service. - -```ts title="src/modules/cms/service.ts" -type CmsModuleOptions = { - apiKey: string -} - -export default class CmsModuleService { - private client - - constructor({}, options: CmsModuleOptions) { - this.client = new Client(options) - } - - async list( - filter: { - id: string | string[] - } - ) { - return this.client.getPosts(filter) - /** - - Example of returned data: - - - - [ - - { - - "id": "post_123", - - "product_id": "prod_321" - - }, - - { - - "id": "post_456", - - "product_id": "prod_654" - - } - - ] - */ - } -} -``` - -The above service initializes a client, assuming your CMS has an SDK that allows you to retrieve posts. - -The service must have a `list` method to be part of the read-only module link. This method accepts the ID(s) of the products to retrieve their associated posts. The posts must include the product's ID in a field, such as `product_id`. - -Next, define a read-only module link from the Product Module to the CMS Module: - -```ts title="src/links/product-cms.ts" -import { defineLink } from "@medusajs/framework/utils" -import ProductModule from "@medusajs/medusa/product" -import { CMS_MODULE } from "../modules/cms" - -export default defineLink( - { - linkable: ProductModule.linkable.product, - field: "id", - }, - { - linkable: { - serviceName: CMS_MODULE, - alias: "cms_post", - primaryKey: "product_id", - }, - }, - { - readOnly: true, - } -) -``` - -To define the read-only module link, you must pass to `defineLink`: - -1. The first parameter: an object with the linkable configuration of the data model in Medusa, and the fields that will be passed as a filter to the CMS service. For example, if you want to filter by product title instead, you can pass `title` instead of `id`. -2. The second parameter: an object with the linkable configuration of the virtual data model in the CMS. This object must have the following properties: - - `serviceName`: The name of the service, which is the CMS Module's name. Medusa uses this name to resolve the module's service from the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md). - - `alias`: The alias to use when querying the linked records. You'll see how that works in a bit. - - `primaryKey`: The field in the CMS data model that holds the ID of a product. -3. The third parameter: an object with the `readOnly` property set to `true`. - -Now, you can use Query to retrieve a product and its linked post from the CMS: - -```ts -const { data } = await query.graph({ - entity: "product", - fields: ["id", "cms_post.*"], -}) -``` - -In the above example, each product that has a CMS post with the `product_id` field set to the product's ID will be retrieved: - -```json title="Example Data" -[ - { - "id": "prod_123", - "cms_post": { - "id": "post_123", - "product_id": "prod_123", - // ... - } - } -] -``` - -If multiple posts have their `product_id` set to a product's ID, an array of posts is returned instead: - -```json title="Example Data" -[ - { - "id": "prod_123", - "cms_post": [ - { - "id": "post_123", - "product_id": "prod_123", - // ... - }, - { - "id": "post_124", - "product_id": "prod_123", - // ... - } - ] - } -] -``` - -[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. - - # Commerce Modules In this chapter, you'll learn about Medusa's Commerce Modules. @@ -13441,6 +13411,38 @@ export default async function helloWorldLoader({ ``` +# Infrastructure Modules + +In this chapter, you’ll learn about Infrastructure Modules. + +## What is an Infrastructure Module? + +An Infrastructure Module implements features and mechanisms related to the Medusa application’s architecture and infrastructure. + +Since modules are interchangeable, you have more control over Medusa’s architecture. For example, you can choose to use Memcached for event handling instead of Redis. + +*** + +## Infrastructure Module Types + +There are different Infrastructure Module types including: + +![Diagram illustrating how the modules connect to third-party services](https://res.cloudinary.com/dza7lstvk/image/upload/v1727095814/Medusa%20Book/infrastructure-modules_bj9bb9.jpg) + +- Cache Module: Defines the caching mechanism or logic to cache computational results. +- Event Module: Integrates a pub/sub service to handle subscribing to and emitting events. +- Workflow Engine Module: Integrates a service to store and track workflow executions and steps. +- File Module: Integrates a storage service to handle uploading and managing files. +- Notification Module: Integrates a third-party service or defines custom logic to send notifications to users and customers. +- Locking Module: Integrates a service that manages access to shared resources by multiple processes or threads. + +*** + +## Infrastructure Modules List + +Refer to the [Infrastructure Modules reference](https://docs.medusajs.com/resources/infrastructure-modules/index.html.md) for a list of Medusa’s Infrastructure Modules, available modules to install, and how to create an Infrastructure Module. + + # Perform Database Operations in a Service In this chapter, you'll learn how to perform database operations in a module's service. @@ -14002,38 +14004,6 @@ export const syncBrandsWorkflow = createWorkflow( You can then use this workflow in an API route, scheduled job, or other resources that use this functionality. -# Infrastructure Modules - -In this chapter, you’ll learn about Infrastructure Modules. - -## What is an Infrastructure Module? - -An Infrastructure Module implements features and mechanisms related to the Medusa application’s architecture and infrastructure. - -Since modules are interchangeable, you have more control over Medusa’s architecture. For example, you can choose to use Memcached for event handling instead of Redis. - -*** - -## Infrastructure Module Types - -There are different Infrastructure Module types including: - -![Diagram illustrating how the modules connect to third-party services](https://res.cloudinary.com/dza7lstvk/image/upload/v1727095814/Medusa%20Book/infrastructure-modules_bj9bb9.jpg) - -- Cache Module: Defines the caching mechanism or logic to cache computational results. -- Event Module: Integrates a pub/sub service to handle subscribing to and emitting events. -- Workflow Engine Module: Integrates a service to store and track workflow executions and steps. -- File Module: Integrates a storage service to handle uploading and managing files. -- Notification Module: Integrates a third-party service or defines custom logic to send notifications to users and customers. -- Locking Module: Integrates a service that manages access to shared resources by multiple processes or threads. - -*** - -## Infrastructure Modules List - -Refer to the [Infrastructure Modules reference](https://docs.medusajs.com/resources/infrastructure-modules/index.html.md) for a list of Medusa’s Infrastructure Modules, available modules to install, and how to create an Infrastructure Module. - - # Loaders In this chapter, you’ll learn about loaders and how to use them. @@ -14277,6 +14247,134 @@ info: Connected to MongoDB You can now resolve the MongoDB Module's main service in your customizations to perform operations on the MongoDB database. +# Multiple Services in a Module + +In this chapter, you'll learn how to use multiple services in a module. + +## Module's Main and Internal Services + +A module has one main service only, which is the service exported in the module's definition. + +However, you may use other services in your module to better organize your code or split functionalities. These are called internal services that can be resolved within your module, but not in external resources. + +*** + +## How to Add an Internal Service + +### 1. Create Service + +To add an internal service, create it in the `services` directory of your module. + +For example, create the file `src/modules/blog/services/client.ts` with the following content: + +```ts title="src/modules/blog/services/client.ts" +export class ClientService { + async getMessage(): Promise { + return "Hello, World!" + } +} +``` + +### 2. Export Service in Index + +Next, create an `index.ts` file under the `services` directory of the module that exports your internal services. + +For example, create the file `src/modules/blog/services/index.ts` with the following content: + +```ts title="src/modules/blog/services/index.ts" +export * from "./client" +``` + +This exports the `ClientService`. + +### 3. Resolve Internal Service + +Internal services exported in the `services/index.ts` file of your module are now registered in the container and can be resolved in other services in the module as well as loaders. + +For example, in your main service: + +```ts title="src/modules/blog/service.ts" highlights={[["5"], ["13"]]} +// other imports... +import { ClientService } from "./services" + +type InjectedDependencies = { + clientService: ClientService +} + +class BlogModuleService extends MedusaService({ + Post, +}){ + protected clientService_: ClientService + + constructor({ clientService }: InjectedDependencies) { + super(...arguments) + this.clientService_ = clientService + } +} +``` + +You can now use your internal service in your main service. + +*** + +## Resolve Resources in Internal Service + +Resolve dependencies from your module's container in the constructor of your internal service. + +For example: + +```ts +import { Logger } from "@medusajs/framework/types" + +type InjectedDependencies = { + logger: Logger +} + +export class ClientService { + protected logger_: Logger + + constructor({ logger }: InjectedDependencies) { + this.logger_ = logger + } +} +``` + +*** + +## Access Module Options + +Your internal service can't access the module's options. + +To retrieve the module's options, use the `configModule` registered in the module's container, which is the configurations in `medusa-config.ts`. + +For example: + +```ts +import { ConfigModule } from "@medusajs/framework/types" +import { BLOG_MODULE } from ".." + +export type InjectedDependencies = { + configModule: ConfigModule +} + +export class ClientService { + protected options: Record + + constructor({ configModule }: InjectedDependencies) { + const moduleDef = configModule.modules[BLOG_MODULE] + + if (typeof moduleDef !== "boolean") { + this.options = moduleDef.options + } + } +} +``` + +The `configModule` has a `modules` property that includes all registered modules. Retrieve the module's configuration using its registration key. + +If its value is not a `boolean`, set the service's options to the module configuration's `options` property. + + # Modules Directory Structure In this document, you'll learn about the expected files and directories in your module. @@ -14507,134 +14605,6 @@ export default BlogModuleService ``` -# 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. @@ -14810,6 +14780,81 @@ export default BlogModuleService ``` +# Scheduled Jobs Number of Executions + +In this chapter, you'll learn how to set a limit on the number of times a scheduled job is executed. + +## numberOfExecutions Option + +The export configuration object of the scheduled job accepts an optional property `numberOfExecutions`. Its value is a number indicating how many times the scheduled job can be executed during the Medusa application's runtime. + +For example: + +```ts highlights={highlights} +export default async function myCustomJob() { + console.log("I'll be executed three times only.") +} + +export const config = { + name: "hello-world", + // execute every minute + schedule: "* * * * *", + numberOfExecutions: 3, +} +``` + +The above scheduled job has the `numberOfExecutions` configuration set to `3`. + +So, it'll only execute 3 times, each every minute, then it won't be executed anymore. + +If you restart the Medusa application, the scheduled job will be executed again until reaching the number of executions specified. + + +# Access Workflow Errors + +In this chapter, you’ll learn how to access errors that occur during a workflow’s execution. + +## How to Access Workflow Errors? + +By default, when an error occurs in a workflow, it throws that error, and the execution stops. + +You can configure the workflow to return the errors instead so that you can access and handle them differently. + +For example: + +```ts title="src/api/workflows/route.ts" highlights={highlights} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import myWorkflow from "../../../workflows/hello-world" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result, errors } = await myWorkflow(req.scope) + .run({ + // ... + throwOnError: false, + }) + + if (errors.length) { + return res.send({ + errors: errors.map((error) => error.error), + }) + } + + res.send(result) +} + +``` + +The object passed to the `run` method accepts a `throwOnError` property. When disabled, the errors are returned in the `errors` property of `run`'s output. + +The value of `errors` is an array of error objects. Each object has an `error` property, whose value is the name or text of the thrown error. + + # Expose a Workflow Hook In this chapter, you'll learn how to expose a hook in your workflow. @@ -14879,51 +14924,6 @@ The hook is available on the workflow's `hooks` property using its name `product You invoke the hook, passing a step function (the hook handler) as a parameter. -# Access Workflow Errors - -In this chapter, you’ll learn how to access errors that occur during a workflow’s execution. - -## How to Access Workflow Errors? - -By default, when an error occurs in a workflow, it throws that error, and the execution stops. - -You can configure the workflow to return the errors instead so that you can access and handle them differently. - -For example: - -```ts title="src/api/workflows/route.ts" highlights={highlights} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import myWorkflow from "../../../workflows/hello-world" - -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result, errors } = await myWorkflow(req.scope) - .run({ - // ... - throwOnError: false, - }) - - if (errors.length) { - return res.send({ - errors: errors.map((error) => error.error), - }) - } - - res.send(result) -} - -``` - -The object passed to the `run` method accepts a `throwOnError` property. When disabled, the errors are returned in the `errors` property of `run`'s output. - -The value of `errors` is an array of error objects. Each object has an `error` property, whose value is the name or text of the thrown error. - - # Compensation Function In this chapter, you'll learn what a compensation function is and how to add it to a step. @@ -17278,6 +17278,137 @@ 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. + +### Prerequisites + +- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) + +## Write Integration Test for Workflow + +Consider you have the following workflow defined at `src/workflows/hello-world.ts`: + +```ts title="src/workflows/hello-world.ts" +import { + createWorkflow, + createStep, + StepResponse, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" + +const step1 = createStep("step-1", () => { + return new StepResponse("Hello, World!") +}) + +export const helloWorldWorkflow = createWorkflow( + "hello-world-workflow", + () => { + const message = step1() + + return new WorkflowResponse(message) + } +) +``` + +To write a test for this workflow, create the file `integration-tests/http/workflow.spec.ts` with the following content: + +```ts title="integration-tests/http/workflow.spec.ts" +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import { helloWorldWorkflow } from "../../src/workflows/hello-world" + +medusaIntegrationTestRunner({ + testSuite: ({ getContainer }) => { + describe("Test hello-world workflow", () => { + it("returns message", async () => { + const { result } = await helloWorldWorkflow(getContainer()) + .run() + + expect(result).toEqual("Hello, World!") + }) + }) + }, +}) + +jest.setTimeout(60 * 1000) +``` + +You use the `medusaIntegrationTestRunner` to write an integration test for the workflow. The test pases if the workflow returns the string `"Hello, World!"`. + +### Jest Timeout + +Since your tests connect to the database and perform actions that require more time than the typical tests, make sure to increase the timeout in your test: + +```ts title="integration-tests/http/custom-routes.spec.ts" +// in your test's file +jest.setTimeout(60 * 1000) +``` + +*** + +## Run Test + +Run the following command to run your tests: + +```bash npm2yarn +npm run test:integration +``` + +If you don't have a `test:integration` script in `package.json`, refer to the [Medusa Testing Tools chapter](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools#add-test-commands/index.html.md). + +This runs your Medusa application and runs the tests available under the `integrations/http` directory. + +*** + +## Test That a Workflow Throws an Error + +You might want to test that a workflow throws an error in certain cases. To test this: + +- Disable the `throwOnError` option when executing the workflow. +- Use the returned `errors` property to check what errors were thrown. + +For example, if you have a step that throws this error: + +```ts title="src/workflows/hello-world.ts" +import { MedusaError } from "@medusajs/framework/utils" +import { createStep } from "@medusajs/framework/workflows-sdk" + +const step1 = createStep("step-1", () => { + throw new MedusaError(MedusaError.Types.NOT_FOUND, "Item doesn't exist") +}) +``` + +You can write the following test to ensure that the workflow throws that error: + +```ts title="integration-tests/http/workflow.spec.ts" +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import { helloWorldWorkflow } from "../../src/workflows/hello-world" + +medusaIntegrationTestRunner({ + testSuite: ({ getContainer }) => { + describe("Test hello-world workflow", () => { + it("returns message", async () => { + const { errors } = await helloWorldWorkflow(getContainer()) + .run({ + throwOnError: false, + }) + + expect(errors.length).toBeGreaterThan(0) + expect(errors[0].error.message).toBe("Item doesn't exist") + }) + }) + }, +}) + +jest.setTimeout(60 * 1000) +``` + +The `errors` property contains an array of errors thrown during the execution of the workflow. Each error item has an `error` object, being the error thrown. + +If you threw a `MedusaError`, then you can check the error message in `errors[0].error.message`. + + # Example: Write Integration Tests for API Routes In this chapter, you'll learn how to write integration tests for API routes using [medusaIntegrationTestRunner](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/integration-tests/index.html.md) from Medusa's Testing Framework. @@ -17847,137 +17978,6 @@ const response = await api.post(`/custom`, form, { ``` -# 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. - -### Prerequisites - -- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) - -## Write Integration Test for Workflow - -Consider you have the following workflow defined at `src/workflows/hello-world.ts`: - -```ts title="src/workflows/hello-world.ts" -import { - createWorkflow, - createStep, - StepResponse, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" - -const step1 = createStep("step-1", () => { - return new StepResponse("Hello, World!") -}) - -export const helloWorldWorkflow = createWorkflow( - "hello-world-workflow", - () => { - const message = step1() - - return new WorkflowResponse(message) - } -) -``` - -To write a test for this workflow, create the file `integration-tests/http/workflow.spec.ts` with the following content: - -```ts title="integration-tests/http/workflow.spec.ts" -import { medusaIntegrationTestRunner } from "@medusajs/test-utils" -import { helloWorldWorkflow } from "../../src/workflows/hello-world" - -medusaIntegrationTestRunner({ - testSuite: ({ getContainer }) => { - describe("Test hello-world workflow", () => { - it("returns message", async () => { - const { result } = await helloWorldWorkflow(getContainer()) - .run() - - expect(result).toEqual("Hello, World!") - }) - }) - }, -}) - -jest.setTimeout(60 * 1000) -``` - -You use the `medusaIntegrationTestRunner` to write an integration test for the workflow. The test pases if the workflow returns the string `"Hello, World!"`. - -### Jest Timeout - -Since your tests connect to the database and perform actions that require more time than the typical tests, make sure to increase the timeout in your test: - -```ts title="integration-tests/http/custom-routes.spec.ts" -// in your test's file -jest.setTimeout(60 * 1000) -``` - -*** - -## Run Test - -Run the following command to run your tests: - -```bash npm2yarn -npm run test:integration -``` - -If you don't have a `test:integration` script in `package.json`, refer to the [Medusa Testing Tools chapter](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools#add-test-commands/index.html.md). - -This runs your Medusa application and runs the tests available under the `integrations/http` directory. - -*** - -## Test That a Workflow Throws an Error - -You might want to test that a workflow throws an error in certain cases. To test this: - -- Disable the `throwOnError` option when executing the workflow. -- Use the returned `errors` property to check what errors were thrown. - -For example, if you have a step that throws this error: - -```ts title="src/workflows/hello-world.ts" -import { MedusaError } from "@medusajs/framework/utils" -import { createStep } from "@medusajs/framework/workflows-sdk" - -const step1 = createStep("step-1", () => { - throw new MedusaError(MedusaError.Types.NOT_FOUND, "Item doesn't exist") -}) -``` - -You can write the following test to ensure that the workflow throws that error: - -```ts title="integration-tests/http/workflow.spec.ts" -import { medusaIntegrationTestRunner } from "@medusajs/test-utils" -import { helloWorldWorkflow } from "../../src/workflows/hello-world" - -medusaIntegrationTestRunner({ - testSuite: ({ getContainer }) => { - describe("Test hello-world workflow", () => { - it("returns message", async () => { - const { errors } = await helloWorldWorkflow(getContainer()) - .run({ - throwOnError: false, - }) - - expect(errors.length).toBeGreaterThan(0) - expect(errors[0].error.message).toBe("Item doesn't exist") - }) - }) - }, -}) - -jest.setTimeout(60 * 1000) -``` - -The `errors` property contains an array of errors thrown during the execution of the workflow. Each error item has an `error` object, being the error thrown. - -If you threw a `MedusaError`, then you can check the error message in `errors[0].error.message`. - - # Example: Integration Tests for a Module In this chapter, find an example of writing an integration test for a module using [moduleIntegrationTestRunner](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/modules-tests/index.html.md) from Medusa's Testing Framework. @@ -18358,24 +18358,29 @@ Medusa provides the following authentication providers out-of-the-box. You can u *** -# Customer Module +# Fulfillment 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. +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/customers/index.html.md) to learn how to manage customers and groups using the dashboard. +Refer to the Medusa Admin User Guide to learn how to use the dashboard to: -Medusa has customer related features available out-of-the-box through the Customer Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Customer Module. +- [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). -## Customer Features +## Fulfillment Features -- [Customer Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/customer-accounts/index.html.md): Store and manage guest and registered customers in your store. -- [Customer Organization](https://docs.medusajs.com/references/customer/models/index.html.md): Organize customers into groups. This has a lot of benefits and supports many use cases, such as provide discounts for specific customer groups using the [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md). +- [Fulfillment Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/item-fulfillment/index.html.md): Create fulfillments and keep track of their status, items, and more. +- [Integrate Third-Party Fulfillment Providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/fulfillment-provider/index.html.md): Create third-party fulfillment providers to provide customers with shipping options and fulfill their orders. +- [Restrict By Location and Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/shipping-option/index.html.md): Shipping options can be restricted to specific geographical locations. You can also specify custom rules to restrict shipping options. +- [Support Different Fulfillment Forms](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/concepts/index.html.md): Support various fulfillment forms, such as shipping or pick up. *** -## How to Use the Customer 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. @@ -18383,7 +18388,7 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-customer.ts" highlights={highlights} +```ts title="src/workflows/create-fulfillment.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -18392,36 +18397,50 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createCustomerStep = createStep( - "create-customer", +const createFulfillmentStep = createStep( + "create-fulfillment", async ({}, { container }) => { - const customerModuleService = container.resolve(Modules.CUSTOMER) + const fulfillmentModuleService = container.resolve(Modules.FULFILLMENT) - const customer = await customerModuleService.createCustomers({ - first_name: "Peter", - last_name: "Hayes", - email: "peter.hayes@example.com", + 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({ customer }, customer.id) + return new StepResponse({ fulfillment }, fulfillment.id) }, - async (customerId, { container }) => { - if (!customerId) { + async (fulfillmentId, { container }) => { + if (!fulfillmentId) { return } - const customerModuleService = container.resolve(Modules.CUSTOMER) + const fulfillmentModuleService = container.resolve(Modules.FULFILLMENT) - await customerModuleService.deleteCustomers([customerId]) + await fulfillmentModuleService.deleteFulfillment(fulfillmentId) } ) -export const createCustomerWorkflow = createWorkflow( - "create-customer", +export const createFulfillmentWorkflow = createWorkflow( + "create-fulfillment", () => { - const { customer } = createCustomerStep() + const { fulfillment } = createFulfillmentStep() return new WorkflowResponse({ - customer, + fulfillment, }) } ) @@ -18436,13 +18455,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createCustomerWorkflow } from "../../workflows/create-customer" +import { createFulfillmentWorkflow } from "../../workflows/create-fuilfillment" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createCustomerWorkflow(req.scope) + const { result } = await createFulfillmentWorkflow(req.scope) .run() res.send(result) @@ -18456,13 +18475,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createCustomerWorkflow } from "../workflows/create-customer" +import { createFulfillmentWorkflow } from "../workflows/create-fuilfillment" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createCustomerWorkflow(container) + const { result } = await createFulfillmentWorkflow(container) .run() console.log(result) @@ -18477,12 +18496,162 @@ export const config: SubscriberConfig = { ```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} import { MedusaContainer } from "@medusajs/framework/types" -import { createCustomerWorkflow } from "../workflows/create-customer" +import { createFulfillmentWorkflow } from "../workflows/create-fuilfillment" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createCustomerWorkflow(container) + 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. + +*** + + +# Inventory Module + +In this section of the documentation, you will find resources to learn more about the Inventory Module and how to use it in your application. + +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/inventory/index.html.md) to learn how to manage inventory and related features using the dashboard. + +Medusa has inventory related features available out-of-the-box through the Inventory Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Inventory Module. + +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). + +## Inventory Features + +- [Inventory Items Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts/index.html.md): Store and manage inventory of any stock-kept item, such as product variants. +- [Inventory Across Locations](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts#inventorylevel/index.html.md): Manage inventory levels across different locations, such as warehouses. +- [Reservation Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts#reservationitem/index.html.md): Reserve quantities of inventory items at specific locations for orders or other purposes. +- [Check Inventory Availability](https://docs.medusajs.com/references/inventory-next/confirmInventory/index.html.md): Check whether an inventory item has the necessary quantity for purchase. +- [Inventory Kits](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-kit/index.html.md): Create and manage inventory kits for a single product, allowing you to implement use cases like bundled or multi-part products. + +*** + +## How to Use the Inventory Module + +In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. + +You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. + +For example: + +```ts title="src/workflows/create-inventory-item.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" + +const createInventoryItemStep = createStep( + "create-inventory-item", + async ({}, { container }) => { + const inventoryModuleService = container.resolve(Modules.INVENTORY) + + const inventoryItem = await inventoryModuleService.createInventoryItems({ + sku: "SHIRT", + title: "Green Medusa Shirt", + requires_shipping: true, + }) + + return new StepResponse({ inventoryItem }, inventoryItem.id) + }, + async (inventoryItemId, { container }) => { + if (!inventoryItemId) { + return + } + const inventoryModuleService = container.resolve(Modules.INVENTORY) + + await inventoryModuleService.deleteInventoryItems([inventoryItemId]) + } +) + +export const createInventoryItemWorkflow = createWorkflow( + "create-inventory-item-workflow", + () => { + const { inventoryItem } = createInventoryItemStep() + + return new WorkflowResponse({ + inventoryItem, + }) + } +) +``` + +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: + +### API Route + +```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { createInventoryItemWorkflow } from "../../workflows/create-inventory-item" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createInventoryItemWorkflow(req.scope) + .run() + + res.send(result) +} +``` + +### Subscriber + +```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import { + type SubscriberConfig, + type SubscriberArgs, +} from "@medusajs/framework" +import { createInventoryItemWorkflow } from "../workflows/create-inventory-item" + +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createInventoryItemWorkflow(container) + .run() + + console.log(result) +} + +export const config: SubscriberConfig = { + event: "user.created", +} +``` + +### Scheduled Job + +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { createInventoryItemWorkflow } from "../workflows/create-inventory-item" + +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createInventoryItemWorkflow(container) .run() console.log(result) @@ -18647,6 +18816,147 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** +# Customer Module + +In this section of the documentation, you will find resources to learn more about the Customer Module and how to use it in your application. + +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/customers/index.html.md) to learn how to manage customers and groups using the dashboard. + +Medusa has customer related features available out-of-the-box through the Customer Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Customer Module. + +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). + +## Customer Features + +- [Customer Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/customer-accounts/index.html.md): Store and manage guest and registered customers in your store. +- [Customer Organization](https://docs.medusajs.com/references/customer/models/index.html.md): Organize customers into groups. This has a lot of benefits and supports many use cases, such as provide discounts for specific customer groups using the [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md). + +*** + +## How to Use the Customer Module + +In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. + +You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. + +For example: + +```ts title="src/workflows/create-customer.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" + +const createCustomerStep = createStep( + "create-customer", + async ({}, { container }) => { + const customerModuleService = container.resolve(Modules.CUSTOMER) + + const customer = await customerModuleService.createCustomers({ + first_name: "Peter", + last_name: "Hayes", + email: "peter.hayes@example.com", + }) + + return new StepResponse({ customer }, customer.id) + }, + async (customerId, { container }) => { + if (!customerId) { + return + } + const customerModuleService = container.resolve(Modules.CUSTOMER) + + await customerModuleService.deleteCustomers([customerId]) + } +) + +export const createCustomerWorkflow = createWorkflow( + "create-customer", + () => { + const { customer } = createCustomerStep() + + return new WorkflowResponse({ + customer, + }) + } +) +``` + +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: + +### API Route + +```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { createCustomerWorkflow } from "../../workflows/create-customer" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createCustomerWorkflow(req.scope) + .run() + + res.send(result) +} +``` + +### Subscriber + +```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import { + type SubscriberConfig, + type SubscriberArgs, +} from "@medusajs/framework" +import { createCustomerWorkflow } from "../workflows/create-customer" + +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createCustomerWorkflow(container) + .run() + + console.log(result) +} + +export const config: SubscriberConfig = { + event: "user.created", +} +``` + +### Scheduled Job + +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { createCustomerWorkflow } from "../workflows/create-customer" + +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createCustomerWorkflow(container) + .run() + + console.log(result) +} + +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, +} +``` + +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). + +*** + + # 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. @@ -18797,316 +19107,6 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Inventory Module - -In this section of the documentation, you will find resources to learn more about the Inventory Module and how to use it in your application. - -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/inventory/index.html.md) to learn how to manage inventory and related features using the dashboard. - -Medusa has inventory related features available out-of-the-box through the Inventory Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Inventory Module. - -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). - -## Inventory Features - -- [Inventory Items Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts/index.html.md): Store and manage inventory of any stock-kept item, such as product variants. -- [Inventory Across Locations](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts#inventorylevel/index.html.md): Manage inventory levels across different locations, such as warehouses. -- [Reservation Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts#reservationitem/index.html.md): Reserve quantities of inventory items at specific locations for orders or other purposes. -- [Check Inventory Availability](https://docs.medusajs.com/references/inventory-next/confirmInventory/index.html.md): Check whether an inventory item has the necessary quantity for purchase. -- [Inventory Kits](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-kit/index.html.md): Create and manage inventory kits for a single product, allowing you to implement use cases like bundled or multi-part products. - -*** - -## How to Use the Inventory Module - -In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. - -You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. - -For example: - -```ts title="src/workflows/create-inventory-item.ts" highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" - -const createInventoryItemStep = createStep( - "create-inventory-item", - async ({}, { container }) => { - const inventoryModuleService = container.resolve(Modules.INVENTORY) - - const inventoryItem = await inventoryModuleService.createInventoryItems({ - sku: "SHIRT", - title: "Green Medusa Shirt", - requires_shipping: true, - }) - - return new StepResponse({ inventoryItem }, inventoryItem.id) - }, - async (inventoryItemId, { container }) => { - if (!inventoryItemId) { - return - } - const inventoryModuleService = container.resolve(Modules.INVENTORY) - - await inventoryModuleService.deleteInventoryItems([inventoryItemId]) - } -) - -export const createInventoryItemWorkflow = createWorkflow( - "create-inventory-item-workflow", - () => { - const { inventoryItem } = createInventoryItemStep() - - return new WorkflowResponse({ - inventoryItem, - }) - } -) -``` - -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: - -### API Route - -```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { createInventoryItemWorkflow } from "../../workflows/create-inventory-item" - -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await createInventoryItemWorkflow(req.scope) - .run() - - res.send(result) -} -``` - -### Subscriber - -```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import { - type SubscriberConfig, - type SubscriberArgs, -} from "@medusajs/framework" -import { createInventoryItemWorkflow } from "../workflows/create-inventory-item" - -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await createInventoryItemWorkflow(container) - .run() - - console.log(result) -} - -export const config: SubscriberConfig = { - event: "user.created", -} -``` - -### Scheduled Job - -```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} -import { MedusaContainer } from "@medusajs/framework/types" -import { createInventoryItemWorkflow } from "../workflows/create-inventory-item" - -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await createInventoryItemWorkflow(container) - .run() - - console.log(result) -} - -export const config = { - name: "run-once-a-day", - schedule: `0 0 * * *`, -} -``` - -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). - -*** - - -# 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. - -*** - -## 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. - -*** - - # 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. @@ -19418,27 +19418,25 @@ Medusa provides the following payment providers out-of-the-box. You can use them *** -# Region Module +# Product Module -In this section of the documentation, you will find resources to learn more about the Region Module and how to use it in your application. +In this section of the documentation, you will find resources to learn more about the Product Module and how to use it in your application. -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/regions/index.html.md) to learn how to manage regions using the dashboard. +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/products/index.html.md) to learn how to manage products using the dashboard. -Medusa has region related features available out-of-the-box through the Region Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Region Module. +Medusa has product related features available out-of-the-box through the Product Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Product Module. Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -*** +## Product Features -## Region Features - -- [Region Management](https://docs.medusajs.com/references/region/models/Region/index.html.md): Manage regions in your store. You can create regions with different currencies and settings. -- [Multi-Currency Support](https://docs.medusajs.com/references/region/models/Region/index.html.md): Each region has a currency. You can support multiple currencies in your store by creating multiple regions. -- [Different Settings Per Region](https://docs.medusajs.com/references/region/models/Region/index.html.md): Each region has its own settings, such as what countries belong to a region or its tax settings. +- [Products Management](https://docs.medusajs.com/references/product/models/Product/index.html.md): Store and manage products. Products have custom options, such as color or size, and each variant in the product sets the value for these options. +- [Product Organization](https://docs.medusajs.com/references/product/models/index.html.md): The Product Module provides different data models used to organize products, including categories, collections, tags, and more. +- [Bundled and Multi-Part Products](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-kit/index.html.md): Create and manage inventory kits for a single product, allowing you to implement use cases like bundled or multi-part products. *** -## How to Use Region Module's Service +## How to Use the Product 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. @@ -19446,7 +19444,7 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-region.ts" highlights={highlights} +```ts title="src/workflows/create-product.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -19455,35 +19453,48 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createRegionStep = createStep( - "create-region", +const createProductStep = createStep( + "create-product", async ({}, { container }) => { - const regionModuleService = container.resolve(Modules.REGION) + const productService = container.resolve(Modules.PRODUCT) - const region = await regionModuleService.createRegions({ - name: "Europe", - currency_code: "eur", + const product = await productService.createProducts({ + title: "Medusa Shirt", + options: [ + { + title: "Color", + values: ["Black", "White"], + }, + ], + variants: [ + { + title: "Black Shirt", + options: { + Color: "Black", + }, + }, + ], }) - return new StepResponse({ region }, region.id) + return new StepResponse({ product }, product.id) }, - async (regionId, { container }) => { - if (!regionId) { + async (productId, { container }) => { + if (!productId) { return } - const regionModuleService = container.resolve(Modules.REGION) + const productService = container.resolve(Modules.PRODUCT) - await regionModuleService.deleteRegions([regionId]) + await productService.deleteProducts([productId]) } ) -export const createRegionWorkflow = createWorkflow( - "create-region", +export const createProductWorkflow = createWorkflow( + "create-product", () => { - const { region } = createRegionStep() + const { product } = createProductStep() return new WorkflowResponse({ - region, + product, }) } ) @@ -19498,13 +19509,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createRegionWorkflow } from "../../workflows/create-region" +import { createProductWorkflow } from "../../workflows/create-product" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createRegionWorkflow(req.scope) + const { result } = await createProductWorkflow(req.scope) .run() res.send(result) @@ -19518,13 +19529,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createRegionWorkflow } from "../workflows/create-region" +import { createProductWorkflow } from "../workflows/create-product" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createRegionWorkflow(container) + const { result } = await createProductWorkflow(container) .run() console.log(result) @@ -19539,160 +19550,12 @@ export const config: SubscriberConfig = { ```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} import { MedusaContainer } from "@medusajs/framework/types" -import { createRegionWorkflow } from "../workflows/create-region" +import { createProductWorkflow } from "../workflows/create-product" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createRegionWorkflow(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). - -*** - - -# 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) + const { result } = await createProductWorkflow(container) .run() console.log(result) @@ -19863,6 +19726,297 @@ 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). + +*** + + +# Region Module + +In this section of the documentation, you will find resources to learn more about the Region Module and how to use it in your application. + +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/regions/index.html.md) to learn how to manage regions using the dashboard. + +Medusa has region related features available out-of-the-box through the Region Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Region Module. + +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). + +*** + +## Region Features + +- [Region Management](https://docs.medusajs.com/references/region/models/Region/index.html.md): Manage regions in your store. You can create regions with different currencies and settings. +- [Multi-Currency Support](https://docs.medusajs.com/references/region/models/Region/index.html.md): Each region has a currency. You can support multiple currencies in your store by creating multiple regions. +- [Different Settings Per Region](https://docs.medusajs.com/references/region/models/Region/index.html.md): Each region has its own settings, such as what countries belong to a region or its tax settings. + +*** + +## How to Use Region 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-region.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" + +const createRegionStep = createStep( + "create-region", + async ({}, { container }) => { + const regionModuleService = container.resolve(Modules.REGION) + + const region = await regionModuleService.createRegions({ + name: "Europe", + currency_code: "eur", + }) + + return new StepResponse({ region }, region.id) + }, + async (regionId, { container }) => { + if (!regionId) { + return + } + const regionModuleService = container.resolve(Modules.REGION) + + await regionModuleService.deleteRegions([regionId]) + } +) + +export const createRegionWorkflow = createWorkflow( + "create-region", + () => { + const { region } = createRegionStep() + + return new WorkflowResponse({ + region, + }) + } +) +``` + +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 { createRegionWorkflow } from "../../workflows/create-region" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createRegionWorkflow(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 { createRegionWorkflow } from "../workflows/create-region" + +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createRegionWorkflow(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 { createRegionWorkflow } from "../workflows/create-region" + +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createRegionWorkflow(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. @@ -20023,160 +20177,6 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Product Module - -In this section of the documentation, you will find resources to learn more about the Product Module and how to use it in your application. - -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/products/index.html.md) to learn how to manage products using the dashboard. - -Medusa has product related features available out-of-the-box through the Product Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Product Module. - -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). - -## Product Features - -- [Products Management](https://docs.medusajs.com/references/product/models/Product/index.html.md): Store and manage products. Products have custom options, such as color or size, and each variant in the product sets the value for these options. -- [Product Organization](https://docs.medusajs.com/references/product/models/index.html.md): The Product Module provides different data models used to organize products, including categories, collections, tags, and more. -- [Bundled and Multi-Part Products](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-kit/index.html.md): Create and manage inventory kits for a single product, allowing you to implement use cases like bundled or multi-part products. - -*** - -## How to Use the Product 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-product.ts" highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" - -const createProductStep = createStep( - "create-product", - async ({}, { container }) => { - const productService = container.resolve(Modules.PRODUCT) - - const product = await productService.createProducts({ - title: "Medusa Shirt", - options: [ - { - title: "Color", - values: ["Black", "White"], - }, - ], - variants: [ - { - title: "Black Shirt", - options: { - Color: "Black", - }, - }, - ], - }) - - return new StepResponse({ product }, product.id) - }, - async (productId, { container }) => { - if (!productId) { - return - } - const productService = container.resolve(Modules.PRODUCT) - - await productService.deleteProducts([productId]) - } -) - -export const createProductWorkflow = createWorkflow( - "create-product", - () => { - const { product } = createProductStep() - - return new WorkflowResponse({ - product, - }) - } -) -``` - -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 { createProductWorkflow } from "../../workflows/create-product" - -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await createProductWorkflow(req.scope) - .run() - - res.send(result) -} -``` - -### Subscriber - -```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import { - type SubscriberConfig, - type SubscriberArgs, -} from "@medusajs/framework" -import { createProductWorkflow } from "../workflows/create-product" - -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await createProductWorkflow(container) - .run() - - console.log(result) -} - -export const config: SubscriberConfig = { - event: "user.created", -} -``` - -### Scheduled Job - -```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} -import { MedusaContainer } from "@medusajs/framework/types" -import { createProductWorkflow } from "../workflows/create-product" - -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await createProductWorkflow(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. @@ -20455,6 +20455,150 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** +# Tax Module + +In this section of the documentation, you will find resources to learn more about the Tax Module and how to use it in your application. + +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md) to learn how to manage tax regions using the dashboard. + +Medusa has tax related features available out-of-the-box through the Tax Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Tax Module. + +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). + +## Tax Features + +- [Tax Settings Per Region](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-region/index.html.md): Set different tax settings for each tax region. +- [Tax Rates and Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-rates-and-rules/index.html.md): Manage each region's default tax rates and override them with conditioned tax rates. +- [Retrieve Tax Lines for carts and orders](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-calculation-with-provider/index.html.md): Calculate and retrieve the tax lines of a cart or order's line items and shipping methods with tax providers. + +*** + +## How to Use Tax Module's Service + +In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. + +You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. + +For example: + +```ts title="src/workflows/create-tax-region.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" + +const createTaxRegionStep = createStep( + "create-tax-region", + async ({}, { container }) => { + const taxModuleService = container.resolve(Modules.TAX) + + const taxRegion = await taxModuleService.createTaxRegions({ + country_code: "us", + }) + + return new StepResponse({ taxRegion }, taxRegion.id) + }, + async (taxRegionId, { container }) => { + if (!taxRegionId) { + return + } + const taxModuleService = container.resolve(Modules.TAX) + + await taxModuleService.deleteTaxRegions([taxRegionId]) + } +) + +export const createTaxRegionWorkflow = createWorkflow( + "create-tax-region", + () => { + const { taxRegion } = createTaxRegionStep() + + return new WorkflowResponse({ taxRegion }) + } +) +``` + +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: + +### API Route + +```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { createTaxRegionWorkflow } from "../../workflows/create-tax-region" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createTaxRegionWorkflow(req.scope) + .run() + + res.send(result) +} +``` + +### Subscriber + +```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import { + type SubscriberConfig, + type SubscriberArgs, +} from "@medusajs/framework" +import { createTaxRegionWorkflow } from "../workflows/create-tax-region" + +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createTaxRegionWorkflow(container) + .run() + + console.log(result) +} + +export const config: SubscriberConfig = { + event: "user.created", +} +``` + +### Scheduled Job + +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { createTaxRegionWorkflow } from "../workflows/create-tax-region" + +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createTaxRegionWorkflow(container) + .run() + + console.log(result) +} + +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, +} +``` + +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). + +*** + +## Configure Tax Module + +The Tax Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/module-options/index.html.md) for details on the module's options. + +*** + + # User Module In this section of the documentation, you will find resources to learn more about the User Module and how to use it in your application. @@ -20602,178 +20746,6 @@ The User Module accepts options for further configurations. Refer to [this docum *** -# Tax Module - -In this section of the documentation, you will find resources to learn more about the Tax Module and how to use it in your application. - -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md) to learn how to manage tax regions using the dashboard. - -Medusa has tax related features available out-of-the-box through the Tax Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Tax Module. - -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). - -## Tax Features - -- [Tax Settings Per Region](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-region/index.html.md): Set different tax settings for each tax region. -- [Tax Rates and Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-rates-and-rules/index.html.md): Manage each region's default tax rates and override them with conditioned tax rates. -- [Retrieve Tax Lines for carts and orders](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-calculation-with-provider/index.html.md): Calculate and retrieve the tax lines of a cart or order's line items and shipping methods with tax providers. - -*** - -## How to Use Tax Module's Service - -In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. - -You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. - -For example: - -```ts title="src/workflows/create-tax-region.ts" highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" - -const createTaxRegionStep = createStep( - "create-tax-region", - async ({}, { container }) => { - const taxModuleService = container.resolve(Modules.TAX) - - const taxRegion = await taxModuleService.createTaxRegions({ - country_code: "us", - }) - - return new StepResponse({ taxRegion }, taxRegion.id) - }, - async (taxRegionId, { container }) => { - if (!taxRegionId) { - return - } - const taxModuleService = container.resolve(Modules.TAX) - - await taxModuleService.deleteTaxRegions([taxRegionId]) - } -) - -export const createTaxRegionWorkflow = createWorkflow( - "create-tax-region", - () => { - const { taxRegion } = createTaxRegionStep() - - return new WorkflowResponse({ taxRegion }) - } -) -``` - -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: - -### API Route - -```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { createTaxRegionWorkflow } from "../../workflows/create-tax-region" - -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await createTaxRegionWorkflow(req.scope) - .run() - - res.send(result) -} -``` - -### Subscriber - -```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import { - type SubscriberConfig, - type SubscriberArgs, -} from "@medusajs/framework" -import { createTaxRegionWorkflow } from "../workflows/create-tax-region" - -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await createTaxRegionWorkflow(container) - .run() - - console.log(result) -} - -export const config: SubscriberConfig = { - event: "user.created", -} -``` - -### Scheduled Job - -```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} -import { MedusaContainer } from "@medusajs/framework/types" -import { createTaxRegionWorkflow } from "../workflows/create-tax-region" - -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await createTaxRegionWorkflow(container) - .run() - - console.log(result) -} - -export const config = { - name: "run-once-a-day", - schedule: `0 0 * * *`, -} -``` - -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). - -*** - -## Configure Tax Module - -The Tax Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/module-options/index.html.md) for details on the module's options. - -*** - - -# API Key Concepts - -In this document, you’ll learn about the different types of API keys, their expiration and verification. - -## API Key Types - -There are two types of API keys: - -- `publishable`: A public key used in client applications, such as a storefront. -- `secret`: A secret key used for authentication and verification purposes, such as an admin user’s authentication token or a password reset token. - -The API key’s type is stored in the `type` property of the [ApiKey data model](https://docs.medusajs.com/references/api-key/models/ApiKey/index.html.md). - -*** - -## API Key Expiration - -An API key expires when it’s revoked using the [revoke method of the module’s main service](https://docs.medusajs.com/references/api-key/revoke/index.html.md). - -The associated token is no longer usable or verifiable. - -*** - -## Token Verification - -To verify a token received as an input or in a request, use the [authenticate method of the module’s main service](https://docs.medusajs.com/references/api-key/authenticate/index.html.md) which validates the token against all non-expired tokens. - - # Links between API Key Module and Other Modules This document showcases the module links defined between the API Key Module and other Commerce Modules. @@ -20872,6 +20844,34 @@ createRemoteLinkStep({ ``` +# API Key Concepts + +In this document, you’ll learn about the different types of API keys, their expiration and verification. + +## API Key Types + +There are two types of API keys: + +- `publishable`: A public key used in client applications, such as a storefront. +- `secret`: A secret key used for authentication and verification purposes, such as an admin user’s authentication token or a password reset token. + +The API key’s type is stored in the `type` property of the [ApiKey data model](https://docs.medusajs.com/references/api-key/models/ApiKey/index.html.md). + +*** + +## API Key Expiration + +An API key expires when it’s revoked using the [revoke method of the module’s main service](https://docs.medusajs.com/references/api-key/revoke/index.html.md). + +The associated token is no longer usable or verifiable. + +*** + +## Token Verification + +To verify a token received as an input or in a request, use the [authenticate method of the module’s main service](https://docs.medusajs.com/references/api-key/authenticate/index.html.md) which validates the token against all non-expired tokens. + + # Authentication Flows with the Auth Main Service In this document, you'll learn how to use the Auth Module's main service's methods to implement authentication flows and reset a user's password. @@ -22108,1565 +22108,6 @@ The page shows the user password fields to enter their new password, then submit - [Storefront Guide: Reset Customer Password](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/reset-password/index.html.md) -# Customer Accounts - -In this document, you’ll learn how registered and unregistered accounts are distinguished in the Medusa application. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/customers/index.html.md) to learn how to manage customers using the dashboard. - -## `has_account` Property - -The [Customer data model](https://docs.medusajs.com/references/customer/models/Customer/index.html.md) has a `has_account` property, which is a boolean that indicates whether a customer is registered. - -When a guest customer places an order, a new `Customer` record is created with `has_account` set to `false`. - -When this or another guest customer registers an account with the same email, a new `Customer` record is created with `has_account` set to `true`. - -*** - -## Email Uniqueness - -The above behavior means that two `Customer` records may exist with the same email. However, the main difference is the `has_account` property's value. - -So, there can only be one guest customer (having `has_account=false`) and one registered customer (having `has_account=true`) with the same email. - - -# Links between Customer Module and Other Modules - -This document showcases the module links defined between the Customer Module and other Commerce Modules. - -## Summary - -The Customer Module has the following links to other modules: - -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. - -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -|| in |Stored|| -| in ||Read-only|| -| in ||Read-only|| - -*** - -## Payment Module - -Medusa defines a link between the `Customer` and `AccountHolder` data models, allowing payment providers to save payment methods for a customer, if the payment provider supports it. - -This link is available starting from Medusa `v2.5.0`. - -### Retrieve with Query - -To retrieve the account holder associated with a customer with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`: - -### query.graph - -```ts -const { data: customers } = await query.graph({ - entity: "customer", - fields: [ - "account_holders.*", - ], -}) - -// customers.account_holders -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: customers } = useQueryGraphStep({ - entity: "customer", - fields: [ - "account_holders.*", - ], -}) - -// customers.account_holders -``` - -### Manage with Link - -To manage the account holders of a customer, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.CUSTOMER]: { - customer_id: "cus_123", - }, - [Modules.PAYMENT]: { - account_holder_id: "acchld_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.CUSTOMER]: { - customer_id: "cus_123", - }, - [Modules.PAYMENT]: { - account_holder_id: "acchld_123", - }, -}) -``` - -*** - -## Cart Module - -Medusa defines a read-only link between the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `Cart` data model and the `Customer` data model. Because the link is read-only from the `Cart`'s side, you can only retrieve the customer of a cart, and not the other way around. - -### Retrieve with Query - -To retrieve the customer of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`: - -### query.graph - -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "customer.*", - ], -}) - -// carts.customer -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "customer.*", - ], -}) - -// carts.customer -``` - -*** - -## Order Module - -Medusa defines a read-only link between the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `Order` data model and the `Customer` data model. Because the link is read-only from the `Order`'s side, you can only retrieve the customer of an order, and not the other way around. - -### Retrieve with Query - -To retrieve the customer of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`: - -### query.graph - -```ts -const { data: orders } = await query.graph({ - entity: "order", - fields: [ - "customer.*", - ], -}) - -// orders.customer -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: orders } = useQueryGraphStep({ - entity: "order", - fields: [ - "customer.*", - ], -}) - -// orders.customer -``` - - -# 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| -|---|---|---|---| -| in ||Read-only|| - -*** - -## Store Module - -The Store Module has a `Currency` data model that stores the supported currencies of a store. However, these currencies don't hold all the details of a currency, such as its name or symbol. - -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 `Currency` 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.supported_currencies -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: stores } = useQueryGraphStep({ - entity: "store", - fields: [ - "supported_currencies.currency.*", - ], -}) - -// stores.supported_currencies -``` - - -# Cart Concepts - -In this document, you’ll get an overview of the main concepts of a cart. - -## Shipping and Billing Addresses - -A cart has a shipping and billing address. Both of these addresses are represented by the [Address data model](https://docs.medusajs.com/references/cart/models/Address/index.html.md). - -![A diagram showcasing the relation between the Cart and Address data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711532392/Medusa%20Resources/cart-addresses_ls6qmv.jpg) - -*** - -## Line Items - -A line item, represented by the [LineItem](https://docs.medusajs.com/references/cart/models/LineItem/index.html.md) data model, is a quantity of a product variant added to the cart. A cart has multiple line items. - -A line item stores some of the product variant’s properties, such as the `product_title` and `product_description`. It also stores data related to the item’s quantity and price. - -In the Medusa application, a product variant is implemented in the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md). - -*** - -## Shipping Methods - -A shipping method, represented by the [ShippingMethod data model](https://docs.medusajs.com/references/cart/models/ShippingMethod/index.html.md), is used to fulfill the items in the cart after the order is placed. A cart can have more than one shipping method. - -In the Medusa application, the shipping method is created from a shipping option, available through the [Fulfillment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/index.html.md). Its ID is stored in the `shipping_option_id` property of the method. - -### data Property - -After an order is placed, you can use a third-party fulfillment provider to fulfill its shipments. - -If the fulfillment provider requires additional custom data to be passed along from the checkout process, set this data in the `ShippingMethod`'s `data` property. - -The `data` property is an object used to store custom data relevant later for fulfillment. - - -# Promotions Adjustments in Carts - -In this document, you’ll learn how a promotion is applied to a cart’s line items and shipping methods using adjustment lines. - -## What are Adjustment Lines? - -An adjustment line indicates a change to an item or a shipping method’s amount. It’s used to apply promotions or discounts on a cart. - -The [LineItemAdjustment](https://docs.medusajs.com/references/cart/models/LineItemAdjustment/index.html.md) data model represents changes on a line item, and the [ShippingMethodAdjustment](https://docs.medusajs.com/references/cart/models/ShippingMethodAdjustment/index.html.md) data model represents changes on a shipping method. - -![A diagram showcasing the relations between other data models and adjustment line models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711534248/Medusa%20Resources/cart-adjustments_k4sttb.jpg) - -The `amount` property of the adjustment line indicates the amount to be discounted from the original amount. Also, the ID of the applied promotion is stored in the `promotion_id` property of the adjustment line. - -*** - -## Discountable Option - -The [LineItem](https://docs.medusajs.com/references/cart/models/LineItem/index.html.md) data model has an `is_discountable` property that indicates whether promotions can be applied to the line item. It’s enabled by default. - -When disabled, a promotion can’t be applied to a line item. In the context of the Promotion Module, the promotion isn’t applied to the line item even if it matches its rules. - -*** - -## Promotion Actions - -When using the Cart and Promotion modules together, such as in the Medusa application, use the [computeActions method of the Promotion Module’s main service](https://docs.medusajs.com/references/promotion/computeActions/index.html.md). It retrieves the actions of line items and shipping methods. - -Learn more about actions in the [Promotion Module’s documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/actions/index.html.md). - -For example: - -```ts collapsibleLines="1-8" expandButtonLabel="Show Imports" -import { - ComputeActionAdjustmentLine, - ComputeActionItemLine, - ComputeActionShippingLine, - // ... -} from "@medusajs/framework/types" - -// retrieve the cart -const cart = await cartModuleService.retrieveCart("cart_123", { - relations: [ - "items.adjustments", - "shipping_methods.adjustments", - ], -}) - -// retrieve line item adjustments -const lineItemAdjustments: ComputeActionItemLine[] = [] -cart.items.forEach((item) => { - const filteredAdjustments = item.adjustments?.filter( - (adjustment) => adjustment.code !== undefined - ) as unknown as ComputeActionAdjustmentLine[] - if (filteredAdjustments.length) { - lineItemAdjustments.push({ - ...item, - adjustments: filteredAdjustments, - }) - } -}) - -// retrieve shipping method adjustments -const shippingMethodAdjustments: ComputeActionShippingLine[] = - [] -cart.shipping_methods.forEach((shippingMethod) => { - const filteredAdjustments = - shippingMethod.adjustments?.filter( - (adjustment) => adjustment.code !== undefined - ) as unknown as ComputeActionAdjustmentLine[] - if (filteredAdjustments.length) { - shippingMethodAdjustments.push({ - ...shippingMethod, - adjustments: filteredAdjustments, - }) - } -}) - -// compute actions -const actions = await promotionModuleService.computeActions( - ["promo_123"], - { - items: lineItemAdjustments, - shipping_methods: shippingMethodAdjustments, - } -) -``` - -The `computeActions` method accepts the existing adjustments of line items and shipping methods to compute the actions accurately. - -Then, use the returned `addItemAdjustment` and `addShippingMethodAdjustment` actions to set the cart’s line item and the shipping method’s adjustments. - -```ts collapsibleLines="1-8" expandButtonLabel="Show Imports" -import { - AddItemAdjustmentAction, - AddShippingMethodAdjustment, - // ... -} from "@medusajs/framework/types" - -// ... - -await cartModuleService.setLineItemAdjustments( - cart.id, - actions.filter( - (action) => action.action === "addItemAdjustment" - ) as AddItemAdjustmentAction[] -) - -await cartModuleService.setShippingMethodAdjustments( - cart.id, - actions.filter( - (action) => - action.action === "addShippingMethodAdjustment" - ) as AddShippingMethodAdjustment[] -) -``` - - -# Tax Lines in Cart Module - -In this document, you’ll learn about tax lines in a cart and how to retrieve tax lines with the Tax Module. - -## What are Tax Lines? - -A tax line indicates the tax rate of a line item or a shipping method. The [LineItemTaxLine data model](https://docs.medusajs.com/references/cart/models/LineItemTaxLine/index.html.md) represents a line item’s tax line, and the [ShippingMethodTaxLine data model](https://docs.medusajs.com/references/cart/models/ShippingMethodTaxLine/index.html.md) represents a shipping method’s tax line. - -![A diagram showcasing the relation between other data models and the tax line models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711534431/Medusa%20Resources/cart-tax-lines_oheaq6.jpg) - -*** - -## Tax Inclusivity - -By default, the tax amount is calculated by taking the tax rate from the line item or shipping method’s amount, and then adding them to the item/method’s subtotal. - -However, line items and shipping methods have an `is_tax_inclusive` property that, when enabled, indicates that the item or method’s price already includes taxes. - -So, instead of calculating the tax rate and adding it to the item/method’s subtotal, it’s calculated as part of the subtotal. - -The following diagram is a simplified showcase of how a subtotal is calculated from the taxes perspective. - -![A diagram showing an example of calculating the subtotal of a line item using its taxes](https://res.cloudinary.com/dza7lstvk/image/upload/v1711535295/Medusa%20Resources/cart-tax-inclusive_shpr3t.jpg) - -For example, if a line item's amount is `5000`, the tax rate is `10`, and tax inclusivity is enabled, the tax amount is 10% of `5000`, which is `500`, making the unit price of the line item `4500`. - -*** - -## Retrieve Tax Lines - -When using the Cart and Tax modules together, you can use the `getTaxLines` method of the Tax Module’s main service. It retrieves the tax lines for a cart’s line items and shipping methods. - -```ts -// retrieve the cart -const cart = await cartModuleService.retrieveCart("cart_123", { - relations: [ - "items.tax_lines", - "shipping_methods.tax_lines", - "shipping_address", - ], -}) - -// retrieve the tax lines -const taxLines = await taxModuleService.getTaxLines( - [ - ...(cart.items as TaxableItemDTO[]), - ...(cart.shipping_methods as TaxableShippingDTO[]), - ], - { - address: { - ...cart.shipping_address, - country_code: - cart.shipping_address.country_code || "us", - }, - } -) -``` - -Then, use the returned tax lines to set the line items and shipping methods’ tax lines: - -```ts -// set line item tax lines -await cartModuleService.setLineItemTaxLines( - cart.id, - taxLines.filter((line) => "line_item_id" in line) -) - -// set shipping method tax lines -await cartModuleService.setLineItemTaxLines( - cart.id, - taxLines.filter((line) => "shipping_line_id" in line) -) -``` - - -# Links between Cart Module and Other Modules - -This document showcases the module links defined between the Cart Module and other Commerce Modules. - -## Summary - -The Cart Module has the following links to other modules: - -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. - -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -|| in |Read-only|| -| in ||Stored|| -|| in |Stored|| -|| in |Read-only|| -|| in |Read-only|| -|| in |Stored|| -|| in |Read-only|| -|| in |Read-only|| - -*** - -## Customer Module - -Medusa defines a read-only link between the `Cart` data model and the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md)'s `Customer` data model. This means you can retrieve the details of a cart's customer, but you don't manage the links in a pivot table in the database. The customer of a cart is determined by the `customer_id` property of the `Cart` data model. - -### Retrieve with Query - -To retrieve the customer of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`: - -### query.graph - -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "customer.*", - ], -}) - -// carts.order -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "customer.*", - ], -}) - -// carts.order -``` - -*** - -## Order Module - -The [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md) provides order-management features. - -Medusa defines a link between the `Cart` and `Order` data models. The cart is linked to the order created once the cart is completed. - -![A diagram showcasing an example of how data models from the Cart and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728375735/Medusa%20Resources/cart-order_ijwmfs.jpg) - -### Retrieve with Query - -To retrieve the order of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `order.*` in `fields`: - -### query.graph - -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "order.*", - ], -}) - -// carts.order -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "order.*", - ], -}) - -// carts.order -``` - -### Manage with Link - -To manage the order of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.ORDER]: { - order_id: "order_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.ORDER]: { - order_id: "order_123", - }, -}) -``` - -*** - -## Payment Module - -The [Payment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/index.html.md) handles payment processing and management. - -Medusa defines a link between the `Cart` and `PaymentCollection` data models. A cart has a payment collection which holds all the authorized payment sessions and payments made related to the cart. - -![A diagram showcasing an example of how data models from the Cart and Payment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711537849/Medusa%20Resources/cart-payment_ixziqm.jpg) - -### Retrieve with Query - -To retrieve the payment collection of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `payment_collection.*` in `fields`: - -### query.graph - -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "payment_collection.*", - ], -}) - -// carts.payment_collection -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "payment_collection.*", - ], -}) - -// carts.payment_collection -``` - -### Manage with Link - -To manage the payment collection of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.PAYMENT]: { - payment_collection_id: "paycol_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.PAYMENT]: { - payment_collection_id: "paycol_123", - }, -}) -``` - -*** - -## Product Module - -Medusa defines read-only links between: - -- the `LineItem` data model and the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md)'s `Product` data model. This means you can retrieve the details of a line item's product, but you don't manage the links in a pivot table in the database. The product of a line item is determined by the `product_id` property of the `LineItem` data model. -- the `LineItem` data model and the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md)'s `ProductVariant` data model. This means you can retrieve the details of a line item's variant, but you don't manage the links in a pivot table in the database. The variant of a line item is determined by the `variant_id` property of the `LineItem` data model. - -### Retrieve with Query - -To retrieve the variant of a line item with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variant.*` in `fields`: - -To retrieve the product, pass `product.*` in `fields`. - -### query.graph - -```ts -const { data: lineItems } = await query.graph({ - entity: "line_item", - fields: [ - "variant.*", - ], -}) - -// lineItems.variant -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: lineItems } = useQueryGraphStep({ - entity: "line_item", - fields: [ - "variant.*", - ], -}) - -// lineItems.variant -``` - -*** - -## Promotion Module - -The [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md) provides discount features. - -Medusa defines a link between the `Cart` and `Promotion` data models. This indicates the promotions applied on a cart. - -![A diagram showcasing an example of how data models from the Cart and Promotion modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711538015/Medusa%20Resources/cart-promotion_kuh9vm.jpg) - -Medusa also defines a read-only link between the `LineItemAdjustment` and `Promotion` data models. This means you can retrieve the details of the promotion applied on a line item, but you don't manage the links in a pivot table in the database. The promotion of a line item is determined by the `promotion_id` property of the `LineItemAdjustment` data model. - -### Retrieve with Query - -To retrieve the promotions of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `promotions.*` in `fields`: - -To retrieve the promotion of a line item adjustment, pass `promotion.*` in `fields`. - -### query.graph - -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "promotions.*", - ], -}) - -// carts.promotions -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "promotions.*", - ], -}) - -// carts.promotions -``` - -### Manage with Link - -To manage the promotions of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", - }, -}) -``` - -*** - -## Region Module - -Medusa defines a read-only link between the `Cart` data model and the [Region Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/region/index.html.md)'s `Region` data model. This means you can retrieve the details of a cart's region, but you don't manage the links in a pivot table in the database. The region of a cart is determined by the `region_id` property of the `Cart` data model. - -### Retrieve with Query - -To retrieve the region of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `region.*` in `fields`: - -### query.graph - -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "region.*", - ], -}) - -// carts.region -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "region.*", - ], -}) - -// carts.region -``` - -*** - -## Sales Channel Module - -Medusa defines a read-only link between the `Cart` data model and the [Sales Channel Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/index.html.md)'s `SalesChannel` data model. This means you can retrieve the details of a cart's sales channel, but you don't manage the links in a pivot table in the database. The sales channel of a cart is determined by the `sales_channel_id` property of the `Cart` data model. - -### Retrieve with Query - -To retrieve the sales channel of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channel.*` in `fields`: - -### query.graph - -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "sales_channel.*", - ], -}) - -// carts.sales_channel -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "sales_channel.*", - ], -}) - -// carts.sales_channel -``` - - -# Inventory Concepts - -In this document, you’ll learn about the main concepts in the Inventory Module, and how data is stored and related. - -## InventoryItem - -An inventory item, represented by the [InventoryItem data model](https://docs.medusajs.com/references/inventory-next/models/InventoryItem/index.html.md), is a stock-kept item, such as a product, whose inventory can be managed. - -The `InventoryItem` data model mainly holds details related to the underlying stock item, but has relations to other data models that include its inventory details. - -![A diagram showcasing the relation between data models in the Inventory Module](https://res.cloudinary.com/dza7lstvk/image/upload/v1709658103/Medusa%20Resources/inventory-architecture_kxr2ql.png) - -### Inventory Shipping Requirement - -An inventory item has a `requires_shipping` field (enabled by default) that indicates whether the item requires shipping. For example, if you're selling a digital license that has limited stock quantity but doesn't require shipping. - -When a product variant is purchased in the Medusa application, this field is used to determine whether the item requires shipping. Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/selling-products/index.html.md). - -*** - -## InventoryLevel - -An inventory level, represented by the [InventoryLevel data model](https://docs.medusajs.com/references/inventory-next/models/InventoryLevel/index.html.md), holds the inventory and quantity details of an inventory item in a specific location. - -It has three quantity-related properties: - -- `stocked_quantity`: The available stock quantity of an item in the associated location. -- `reserved_quantity`: The quantity reserved from the available `stocked_quantity`. It indicates the quantity that's still not removed from stock, but considered as unavailable when checking whether an item is in stock. -- `incoming_quantity`: The incoming stock quantity of an item into the associated location. This property doesn't play into the `stocked_quantity` or when checking whether an item is in stock. - -### Associated Location - -The inventory level's location is determined by the `location_id` property. Medusa links the `InventoryLevel` data model with the `StockLocation` data model from the Stock Location Module. - -*** - -## ReservationItem - -A reservation item, represented by the [ReservationItem](https://docs.medusajs.com/references/inventory-next/models/ReservationItem/index.html.md) data model, represents unavailable quantity of an inventory item in a location. It's used when an order is placed but not fulfilled yet. - -The reserved quantity is associated with a location, so it has a similar relation to that of the `InventoryLevel` with the Stock Location Module. - - -# Inventory Kits - -In this guide, you'll learn how inventory kits can be used in the Medusa application to support use cases like multi-part products, bundled products, and shared inventory across products. - -Refer to the following user guides to learn how to use the Medusa Admin dashboard to: - -- [Create Multi-Part Products](https://docs.medusajs.com/user-guide/products/create/multi-part/index.html.md). -- [Create Bundled Products](https://docs.medusajs.com/user-guide/products/create/bundle/index.html.md). - -## What is an Inventory Kit? - -An inventory kit is a collection of inventory items that are linked to a single product variant. These inventory items can be used to represent different parts of a product, or to represent a bundle of products. - -The Medusa application links inventory items from the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md) to product variants in the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md). Each variant can have multiple inventory items, and these inventory items can be re-used or shared across variants. - -Using inventory kits, you can implement use cases like: - -- [Multi-part products](#multi-part-products): A product that consists of multiple parts, each with its own inventory item. -- [Bundled products](#bundled-products): A product that is sold as a bundle, where each variant in the bundle product can re-use the inventory items of another product that should be sold as part of the bundle. - -*** - -## Multi-Part Products - -Consider your store sells bicycles that consist of a frame, wheels, and seats, and you want to manage the inventory of these parts separately. - -To implement this in Medusa, you can: - -- Create inventory items for each of the different parts. -- For each bicycle product, add a variant whose inventory kit consists of the inventory items of each of the parts. - -Then, whenever a customer purchases a bicycle, the inventory of each part is updated accordingly. You can also use the `required_quantity` of the variant's inventory items to set how much quantity is consumed of the part's inventory when a bicycle is sold. For example, the bicycle's wheels require 2 wheels inventory items to be sold when a bicycle is sold. - -![Diagram showcasing how a variant is linked to multi-part inventory items](https://res.cloudinary.com/dza7lstvk/image/upload/v1736414257/Medusa%20Resources/multi-part-product_kepbnx.jpg) - -### Create Multi-Part Product - -Using the [Medusa Admin](https://docs.medusajs.com/user-guide/products/create/multi-part/index.html.md), you can create a multi-part product by creating its inventory items first, then assigning these inventory items to the product's variant(s). - -Using [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), you can implement this by first creating the inventory items: - -```ts highlights={multiPartsHighlights1} -import { - createInventoryItemsWorkflow, - useQueryGraphStep, -} from "@medusajs/medusa/core-flows" -import { createWorkflow } from "@medusajs/framework/workflows-sdk" - -export const createMultiPartProductsWorkflow = createWorkflow( - "create-multi-part-products", - () => { - // Alternatively, you can create a stock location - const { data: stockLocations } = useQueryGraphStep({ - entity: "stock_location", - fields: ["*"], - filters: { - name: "European Warehouse", - }, - }) - - const inventoryItems = createInventoryItemsWorkflow.runAsStep({ - input: { - items: [ - { - sku: "FRAME", - title: "Frame", - location_levels: [ - { - stocked_quantity: 100, - location_id: stockLocations[0].id, - }, - ], - }, - { - sku: "WHEEL", - title: "Wheel", - location_levels: [ - { - stocked_quantity: 100, - location_id: stockLocations[0].id, - }, - ], - }, - { - sku: "SEAT", - title: "Seat", - location_levels: [ - { - stocked_quantity: 100, - location_id: stockLocations[0].id, - }, - ], - }, - ], - }, - }) - - // TODO create the product - } -) -``` - -You start by retrieving the stock location to create the inventory items in. Alternatively, you can [create a stock location](https://docs.medusajs.com/references/medusa-workflows/createStockLocationsWorkflow/index.html.md). - -Then, you create the inventory items that the product variant consists of. - -Next, create the product and pass the inventory item's IDs to the product's variant: - -```ts highlights={multiPartHighlights2} -import { - // ... - transform, -} from "@medusajs/framework/workflows-sdk" -import { - // ... - createProductsWorkflow, -} from "@medusajs/medusa/core-flows" - -export const createMultiPartProductsWorkflow = createWorkflow( - "create-multi-part-products", - () => { - // ... - - const inventoryItemIds = transform({ - inventoryItems, - }, (data) => { - return data.inventoryItems.map((inventoryItem) => { - return { - inventory_item_id: inventoryItem.id, - // can also specify required_quantity - } - }) - }) - - const products = createProductsWorkflow.runAsStep({ - input: { - products: [ - { - title: "Bicycle", - variants: [ - { - title: "Bicycle - Small", - prices: [ - { - amount: 100, - currency_code: "usd", - }, - ], - options: { - "Default Option": "Default Variant", - }, - inventory_items: inventoryItemIds, - }, - ], - options: [ - { - title: "Default Option", - values: ["Default Variant"], - }, - ], - shipping_profile_id: "sp_123", - }, - ], - }, - }) - } -) -``` - -You prepare the inventory item IDs to pass to the variant using [transform](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) from the Workflows SDK, then pass these IDs to the created product's variant. - -You can now [execute the workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows#3-execute-the-workflow/index.html.md) in [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md), [scheduled jobs](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md), or [subscribers](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md). - -*** - -## Bundled Products - -Consider you have three products: shirt, pants, and shoes. You sell those products separately, but you also want to offer them as a bundle. - -![Diagram showcasing products each having their own variants and inventory](https://res.cloudinary.com/dza7lstvk/image/upload/v1736414787/Medusa%20Resources/bundled-product-1_vmzewk.jpg) - -You can do that by creating a product, where each variant re-uses the inventory items of each of the shirt, pants, and shoes products. - -Then, when the bundled product's variant is purchased, the inventory quantity of the associated inventory items are updated. - -![Diagram showcasing a bundled product using the same inventory as the products part of the bundle](https://res.cloudinary.com/dza7lstvk/image/upload/v1736414780/Medusa%20Resources/bundled-product_x94ca1.jpg) - -### Create Bundled Product - -You can create a bundled product in the [Medusa Admin](https://docs.medusajs.com/user-guide/products/create/bundle/index.html.md) by creating the products part of the bundle first, each having its own inventory items. Then, you create the bundled product whose variant(s) have inventory kits composed of inventory items from each of the products part of the bundle. - -Using [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), you can implement this by first creating the products part of the bundle: - -```ts highlights={bundledHighlights1} -import { - createWorkflow, -} from "@medusajs/framework/workflows-sdk" -import { - createProductsWorkflow, -} from "@medusajs/medusa/core-flows" - -export const createBundledProducts = createWorkflow( - "create-bundled-products", - () => { - const products = createProductsWorkflow.runAsStep({ - input: { - products: [ - { - title: "Shirt", - shipping_profile_id: "sp_123", - variants: [ - { - title: "Shirt", - prices: [ - { - amount: 10, - currency_code: "usd", - }, - ], - options: { - "Default Option": "Default Variant", - }, - manage_inventory: true, - }, - ], - options: [ - { - title: "Default Option", - values: ["Default Variant"], - }, - ], - }, - { - title: "Pants", - shipping_profile_id: "sp_123", - variants: [ - { - title: "Pants", - prices: [ - { - amount: 10, - currency_code: "usd", - }, - ], - options: { - "Default Option": "Default Variant", - }, - manage_inventory: true, - }, - ], - options: [ - { - title: "Default Option", - values: ["Default Variant"], - }, - ], - }, - { - title: "Shoes", - shipping_profile_id: "sp_123", - variants: [ - { - title: "Shoes", - prices: [ - { - amount: 10, - currency_code: "usd", - }, - ], - options: { - "Default Option": "Default Variant", - }, - manage_inventory: true, - }, - ], - options: [ - { - title: "Default Option", - values: ["Default Variant"], - }, - ], - }, - ], - }, - }) - - // TODO re-retrieve with inventory - } -) -``` - -You create three products and enable `manage_inventory` for their variants, which will create a default inventory item. You can also create the inventory item first for more control over the quantity as explained in [the previous section](#create-multi-part-product). - -Next, retrieve the products again but with variant information: - -```ts highlights={bundledHighlights2} -import { - // ... - transform, -} from "@medusajs/framework/workflows-sdk" -import { - useQueryGraphStep, -} from "@medusajs/medusa/core-flows" - -export const createBundledProducts = createWorkflow( - "create-bundled-products", - () => { - // ... - const productIds = transform({ - products, - }, (data) => data.products.map((product) => product.id)) - - // @ts-ignore - const { data: productsWithInventory } = useQueryGraphStep({ - entity: "product", - fields: [ - "variants.*", - "variants.inventory_items.*", - ], - filters: { - id: productIds, - }, - }) - - const inventoryItemIds = transform({ - productsWithInventory, - }, (data) => { - return data.productsWithInventory.map((product) => { - return { - inventory_item_id: product.variants[0].inventory_items?.[0]?.inventory_item_id, - } - }) - }) - - // create bundled product - } -) -``` - -Using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), you retrieve the product again with the inventory items of each variant. Then, you prepare the inventory items to pass to the bundled product's variant. - -Finally, create the bundled product: - -```ts highlights={bundledProductHighlights3} -export const createBundledProducts = createWorkflow( - "create-bundled-products", - () => { - // ... - const bundledProduct = createProductsWorkflow.runAsStep({ - input: { - products: [ - { - title: "Bundled Clothes", - shipping_profile_id: "sp_123", - variants: [ - { - title: "Bundle", - prices: [ - { - amount: 30, - currency_code: "usd", - }, - ], - options: { - "Default Option": "Default Variant", - }, - inventory_items: inventoryItemIds, - }, - ], - options: [ - { - title: "Default Option", - values: ["Default Variant"], - }, - ], - }, - ], - }, - }).config({ name: "create-bundled-product" }) - } -) -``` - -The bundled product has the same inventory items as those of the products part of the bundle. - -You can now [execute the workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows#3-execute-the-workflow/index.html.md) in [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md), [scheduled jobs](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md), or [subscribers](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md). - - -# Inventory Module in Medusa Flows - -This document explains how the Inventory Module is used within the Medusa application's flows. - -## Product Variant Creation - -When a product variant is created and its `manage_inventory` property's value is `true`, the Medusa application creates an inventory item associated with that product variant. - -This flow is implemented within the [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/index.html.md) - -![A diagram showcasing how the Inventory Module is used in the product variant creation form](https://res.cloudinary.com/dza7lstvk/image/upload/v1709661511/Medusa%20Resources/inventory-product-create_khz2hk.jpg) - -*** - -## Add to Cart - -When a product variant with `manage_inventory` set to `true` is added to cart, the Medusa application checks whether there's sufficient stocked quantity. If not, an error is thrown and the product variant won't be added to the cart. - -This flow is implemented within the [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md) - -![A diagram showcasing how the Inventory Module is used in the add to cart flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709711645/Medusa%20Resources/inventory-cart-flow_achwq9.jpg) - -*** - -## Order Placed - -When an order is placed, the Medusa application creates a reservation item for each product variant with `manage_inventory` set to `true`. - -This flow is implemented within the [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) - -![A diagram showcasing how the Inventory Module is used in the order placed flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709712005/Medusa%20Resources/inventory-order-placed_qdxqdn.jpg) - -*** - -## Order Fulfillment - -When an item in an order is fulfilled and the associated variant has its `manage_inventory` property set to `true`, the Medusa application: - -- Subtracts the `reserved_quantity` from the `stocked_quantity` in the inventory level associated with the variant's inventory item. -- Resets the `reserved_quantity` to `0`. -- Deletes the associated reservation item. - -This flow is implemented within the [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md) - -![A diagram showcasing how the Inventory Module is used in the order fulfillment flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709712390/Medusa%20Resources/inventory-order-fulfillment_o9wdxh.jpg) - -*** - -## Order Return - -When an item in an order is returned and the associated variant has its `manage_inventory` property set to `true`, the Medusa application increments the `stocked_quantity` of the inventory item's level with the returned quantity. - -This flow is implemented within the [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/index.html.md) - -![A diagram showcasing how the Inventory Module is used in the order return flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709712457/Medusa%20Resources/inventory-order-return_ihftyk.jpg) - -### Dismissed Returned Items - -If a returned item is considered damaged or is dismissed, its quantity doesn't increment the `stocked_quantity` of the inventory item's level. - - -# Links between Inventory Module and Other Modules - -This document showcases the module links defined between the Inventory Module and other Commerce Modules. - -## Summary - -The Inventory Module has the following links to other modules: - -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. - -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -| in ||Stored|| -|| in |Read-only|| - -*** - -## Product Module - -Each product variant has different inventory details. Medusa defines a link between the `ProductVariant` and `InventoryItem` data models. - -![A diagram showcasing an example of how data models from the Inventory and Product Module are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709658720/Medusa%20Resources/inventory-product_ejnray.jpg) - -A product variant whose `manage_inventory` property is enabled has an associated inventory item. Through that inventory's items relations in the Inventory Module, you can manage and check the variant's inventory quantity. - -Learn more about product variant's inventory management in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/variant-inventory/index.html.md). - -### Retrieve with Query - -To retrieve the product variants of an inventory item with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variants.*` in `fields`: - -### query.graph - -```ts -const { data: inventoryItems } = await query.graph({ - entity: "inventory_item", - fields: [ - "variants.*", - ], -}) - -// inventoryItems.variants -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: inventoryItems } = useQueryGraphStep({ - entity: "inventory_item", - fields: [ - "variants.*", - ], -}) - -// inventoryItems.variants -``` - -### Manage with Link - -To manage the variants of an inventory item, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.PRODUCT]: { - variant_id: "variant_123", - }, - [Modules.INVENTORY]: { - inventory_item_id: "iitem_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.PRODUCT]: { - variant_id: "variant_123", - }, - [Modules.INVENTORY]: { - inventory_item_id: "iitem_123", - }, -}) -``` - -*** - -## Stock Location Module - -Medusa defines a read-only link between the `InventoryLevel` data model and the [Stock Location Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/index.html.md)'s `StockLocation` data model. This means you can retrieve the details of an inventory level's stock locations, but you don't manage the links in a pivot table in the database. The stock location of an inventory level is determined by the `location_id` property of the `InventoryLevel` data model. - -### Retrieve with Query - -To retrieve the stock locations of an inventory level with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `stock_locations.*` in `fields`: - -### query.graph - -```ts -const { data: inventoryLevels } = await query.graph({ - entity: "inventory_level", - fields: [ - "stock_locations.*", - ], -}) - -// inventoryLevels.stock_locations -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: inventoryLevels } = useQueryGraphStep({ - entity: "inventory_level", - fields: [ - "stock_locations.*", - ], -}) - -// inventoryLevels.stock_locations -``` - - # Fulfillment Concepts In this document, you’ll learn about some basic fulfillment concepts. @@ -24230,6 +22671,8 @@ Service zones can be more restrictive, such as restricting to certain cities or 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`. @@ -24262,6 +22705,1614 @@ When fulfilling an item, you might use a third-party fulfillment provider that r The `ShippingOption` data model has a `data` property. It's an object that stores custom data relevant later when creating and processing a fulfillment. +# Inventory Concepts + +In this document, you’ll learn about the main concepts in the Inventory Module, and how data is stored and related. + +## InventoryItem + +An inventory item, represented by the [InventoryItem data model](https://docs.medusajs.com/references/inventory-next/models/InventoryItem/index.html.md), is a stock-kept item, such as a product, whose inventory can be managed. + +The `InventoryItem` data model mainly holds details related to the underlying stock item, but has relations to other data models that include its inventory details. + +![A diagram showcasing the relation between data models in the Inventory Module](https://res.cloudinary.com/dza7lstvk/image/upload/v1709658103/Medusa%20Resources/inventory-architecture_kxr2ql.png) + +### Inventory Shipping Requirement + +An inventory item has a `requires_shipping` field (enabled by default) that indicates whether the item requires shipping. For example, if you're selling a digital license that has limited stock quantity but doesn't require shipping. + +When a product variant is purchased in the Medusa application, this field is used to determine whether the item requires shipping. Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/selling-products/index.html.md). + +*** + +## InventoryLevel + +An inventory level, represented by the [InventoryLevel data model](https://docs.medusajs.com/references/inventory-next/models/InventoryLevel/index.html.md), holds the inventory and quantity details of an inventory item in a specific location. + +It has three quantity-related properties: + +- `stocked_quantity`: The available stock quantity of an item in the associated location. +- `reserved_quantity`: The quantity reserved from the available `stocked_quantity`. It indicates the quantity that's still not removed from stock, but considered as unavailable when checking whether an item is in stock. +- `incoming_quantity`: The incoming stock quantity of an item into the associated location. This property doesn't play into the `stocked_quantity` or when checking whether an item is in stock. + +### Associated Location + +The inventory level's location is determined by the `location_id` property. Medusa links the `InventoryLevel` data model with the `StockLocation` data model from the Stock Location Module. + +*** + +## ReservationItem + +A reservation item, represented by the [ReservationItem](https://docs.medusajs.com/references/inventory-next/models/ReservationItem/index.html.md) data model, represents unavailable quantity of an inventory item in a location. It's used when an order is placed but not fulfilled yet. + +The reserved quantity is associated with a location, so it has a similar relation to that of the `InventoryLevel` with the Stock Location Module. + + +# Inventory Module in Medusa Flows + +This document explains how the Inventory Module is used within the Medusa application's flows. + +## Product Variant Creation + +When a product variant is created and its `manage_inventory` property's value is `true`, the Medusa application creates an inventory item associated with that product variant. + +This flow is implemented within the [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/index.html.md) + +![A diagram showcasing how the Inventory Module is used in the product variant creation form](https://res.cloudinary.com/dza7lstvk/image/upload/v1709661511/Medusa%20Resources/inventory-product-create_khz2hk.jpg) + +*** + +## Add to Cart + +When a product variant with `manage_inventory` set to `true` is added to cart, the Medusa application checks whether there's sufficient stocked quantity. If not, an error is thrown and the product variant won't be added to the cart. + +This flow is implemented within the [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md) + +![A diagram showcasing how the Inventory Module is used in the add to cart flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709711645/Medusa%20Resources/inventory-cart-flow_achwq9.jpg) + +*** + +## Order Placed + +When an order is placed, the Medusa application creates a reservation item for each product variant with `manage_inventory` set to `true`. + +This flow is implemented within the [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) + +![A diagram showcasing how the Inventory Module is used in the order placed flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709712005/Medusa%20Resources/inventory-order-placed_qdxqdn.jpg) + +*** + +## Order Fulfillment + +When an item in an order is fulfilled and the associated variant has its `manage_inventory` property set to `true`, the Medusa application: + +- Subtracts the `reserved_quantity` from the `stocked_quantity` in the inventory level associated with the variant's inventory item. +- Resets the `reserved_quantity` to `0`. +- Deletes the associated reservation item. + +This flow is implemented within the [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md) + +![A diagram showcasing how the Inventory Module is used in the order fulfillment flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709712390/Medusa%20Resources/inventory-order-fulfillment_o9wdxh.jpg) + +*** + +## Order Return + +When an item in an order is returned and the associated variant has its `manage_inventory` property set to `true`, the Medusa application increments the `stocked_quantity` of the inventory item's level with the returned quantity. + +This flow is implemented within the [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/index.html.md) + +![A diagram showcasing how the Inventory Module is used in the order return flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709712457/Medusa%20Resources/inventory-order-return_ihftyk.jpg) + +### Dismissed Returned Items + +If a returned item is considered damaged or is dismissed, its quantity doesn't increment the `stocked_quantity` of the inventory item's level. + + +# Inventory Kits + +In this guide, you'll learn how inventory kits can be used in the Medusa application to support use cases like multi-part products, bundled products, and shared inventory across products. + +Refer to the following user guides to learn how to use the Medusa Admin dashboard to: + +- [Create Multi-Part Products](https://docs.medusajs.com/user-guide/products/create/multi-part/index.html.md). +- [Create Bundled Products](https://docs.medusajs.com/user-guide/products/create/bundle/index.html.md). + +## What is an Inventory Kit? + +An inventory kit is a collection of inventory items that are linked to a single product variant. These inventory items can be used to represent different parts of a product, or to represent a bundle of products. + +The Medusa application links inventory items from the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md) to product variants in the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md). Each variant can have multiple inventory items, and these inventory items can be re-used or shared across variants. + +Using inventory kits, you can implement use cases like: + +- [Multi-part products](#multi-part-products): A product that consists of multiple parts, each with its own inventory item. +- [Bundled products](#bundled-products): A product that is sold as a bundle, where each variant in the bundle product can re-use the inventory items of another product that should be sold as part of the bundle. + +*** + +## Multi-Part Products + +Consider your store sells bicycles that consist of a frame, wheels, and seats, and you want to manage the inventory of these parts separately. + +To implement this in Medusa, you can: + +- Create inventory items for each of the different parts. +- For each bicycle product, add a variant whose inventory kit consists of the inventory items of each of the parts. + +Then, whenever a customer purchases a bicycle, the inventory of each part is updated accordingly. You can also use the `required_quantity` of the variant's inventory items to set how much quantity is consumed of the part's inventory when a bicycle is sold. For example, the bicycle's wheels require 2 wheels inventory items to be sold when a bicycle is sold. + +![Diagram showcasing how a variant is linked to multi-part inventory items](https://res.cloudinary.com/dza7lstvk/image/upload/v1736414257/Medusa%20Resources/multi-part-product_kepbnx.jpg) + +### Create Multi-Part Product + +Using the [Medusa Admin](https://docs.medusajs.com/user-guide/products/create/multi-part/index.html.md), you can create a multi-part product by creating its inventory items first, then assigning these inventory items to the product's variant(s). + +Using [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), you can implement this by first creating the inventory items: + +```ts highlights={multiPartsHighlights1} +import { + createInventoryItemsWorkflow, + useQueryGraphStep, +} from "@medusajs/medusa/core-flows" +import { createWorkflow } from "@medusajs/framework/workflows-sdk" + +export const createMultiPartProductsWorkflow = createWorkflow( + "create-multi-part-products", + () => { + // Alternatively, you can create a stock location + const { data: stockLocations } = useQueryGraphStep({ + entity: "stock_location", + fields: ["*"], + filters: { + name: "European Warehouse", + }, + }) + + const inventoryItems = createInventoryItemsWorkflow.runAsStep({ + input: { + items: [ + { + sku: "FRAME", + title: "Frame", + location_levels: [ + { + stocked_quantity: 100, + location_id: stockLocations[0].id, + }, + ], + }, + { + sku: "WHEEL", + title: "Wheel", + location_levels: [ + { + stocked_quantity: 100, + location_id: stockLocations[0].id, + }, + ], + }, + { + sku: "SEAT", + title: "Seat", + location_levels: [ + { + stocked_quantity: 100, + location_id: stockLocations[0].id, + }, + ], + }, + ], + }, + }) + + // TODO create the product + } +) +``` + +You start by retrieving the stock location to create the inventory items in. Alternatively, you can [create a stock location](https://docs.medusajs.com/references/medusa-workflows/createStockLocationsWorkflow/index.html.md). + +Then, you create the inventory items that the product variant consists of. + +Next, create the product and pass the inventory item's IDs to the product's variant: + +```ts highlights={multiPartHighlights2} +import { + // ... + transform, +} from "@medusajs/framework/workflows-sdk" +import { + // ... + createProductsWorkflow, +} from "@medusajs/medusa/core-flows" + +export const createMultiPartProductsWorkflow = createWorkflow( + "create-multi-part-products", + () => { + // ... + + const inventoryItemIds = transform({ + inventoryItems, + }, (data) => { + return data.inventoryItems.map((inventoryItem) => { + return { + inventory_item_id: inventoryItem.id, + // can also specify required_quantity + } + }) + }) + + const products = createProductsWorkflow.runAsStep({ + input: { + products: [ + { + title: "Bicycle", + variants: [ + { + title: "Bicycle - Small", + prices: [ + { + amount: 100, + currency_code: "usd", + }, + ], + options: { + "Default Option": "Default Variant", + }, + inventory_items: inventoryItemIds, + }, + ], + options: [ + { + title: "Default Option", + values: ["Default Variant"], + }, + ], + shipping_profile_id: "sp_123", + }, + ], + }, + }) + } +) +``` + +You prepare the inventory item IDs to pass to the variant using [transform](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) from the Workflows SDK, then pass these IDs to the created product's variant. + +You can now [execute the workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows#3-execute-the-workflow/index.html.md) in [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md), [scheduled jobs](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md), or [subscribers](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md). + +*** + +## Bundled Products + +Consider you have three products: shirt, pants, and shoes. You sell those products separately, but you also want to offer them as a bundle. + +![Diagram showcasing products each having their own variants and inventory](https://res.cloudinary.com/dza7lstvk/image/upload/v1736414787/Medusa%20Resources/bundled-product-1_vmzewk.jpg) + +You can do that by creating a product, where each variant re-uses the inventory items of each of the shirt, pants, and shoes products. + +Then, when the bundled product's variant is purchased, the inventory quantity of the associated inventory items are updated. + +![Diagram showcasing a bundled product using the same inventory as the products part of the bundle](https://res.cloudinary.com/dza7lstvk/image/upload/v1736414780/Medusa%20Resources/bundled-product_x94ca1.jpg) + +### Create Bundled Product + +You can create a bundled product in the [Medusa Admin](https://docs.medusajs.com/user-guide/products/create/bundle/index.html.md) by creating the products part of the bundle first, each having its own inventory items. Then, you create the bundled product whose variant(s) have inventory kits composed of inventory items from each of the products part of the bundle. + +Using [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), you can implement this by first creating the products part of the bundle: + +```ts highlights={bundledHighlights1} +import { + createWorkflow, +} from "@medusajs/framework/workflows-sdk" +import { + createProductsWorkflow, +} from "@medusajs/medusa/core-flows" + +export const createBundledProducts = createWorkflow( + "create-bundled-products", + () => { + const products = createProductsWorkflow.runAsStep({ + input: { + products: [ + { + title: "Shirt", + shipping_profile_id: "sp_123", + variants: [ + { + title: "Shirt", + prices: [ + { + amount: 10, + currency_code: "usd", + }, + ], + options: { + "Default Option": "Default Variant", + }, + manage_inventory: true, + }, + ], + options: [ + { + title: "Default Option", + values: ["Default Variant"], + }, + ], + }, + { + title: "Pants", + shipping_profile_id: "sp_123", + variants: [ + { + title: "Pants", + prices: [ + { + amount: 10, + currency_code: "usd", + }, + ], + options: { + "Default Option": "Default Variant", + }, + manage_inventory: true, + }, + ], + options: [ + { + title: "Default Option", + values: ["Default Variant"], + }, + ], + }, + { + title: "Shoes", + shipping_profile_id: "sp_123", + variants: [ + { + title: "Shoes", + prices: [ + { + amount: 10, + currency_code: "usd", + }, + ], + options: { + "Default Option": "Default Variant", + }, + manage_inventory: true, + }, + ], + options: [ + { + title: "Default Option", + values: ["Default Variant"], + }, + ], + }, + ], + }, + }) + + // TODO re-retrieve with inventory + } +) +``` + +You create three products and enable `manage_inventory` for their variants, which will create a default inventory item. You can also create the inventory item first for more control over the quantity as explained in [the previous section](#create-multi-part-product). + +Next, retrieve the products again but with variant information: + +```ts highlights={bundledHighlights2} +import { + // ... + transform, +} from "@medusajs/framework/workflows-sdk" +import { + useQueryGraphStep, +} from "@medusajs/medusa/core-flows" + +export const createBundledProducts = createWorkflow( + "create-bundled-products", + () => { + // ... + const productIds = transform({ + products, + }, (data) => data.products.map((product) => product.id)) + + // @ts-ignore + const { data: productsWithInventory } = useQueryGraphStep({ + entity: "product", + fields: [ + "variants.*", + "variants.inventory_items.*", + ], + filters: { + id: productIds, + }, + }) + + const inventoryItemIds = transform({ + productsWithInventory, + }, (data) => { + return data.productsWithInventory.map((product) => { + return { + inventory_item_id: product.variants[0].inventory_items?.[0]?.inventory_item_id, + } + }) + }) + + // create bundled product + } +) +``` + +Using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), you retrieve the product again with the inventory items of each variant. Then, you prepare the inventory items to pass to the bundled product's variant. + +Finally, create the bundled product: + +```ts highlights={bundledProductHighlights3} +export const createBundledProducts = createWorkflow( + "create-bundled-products", + () => { + // ... + const bundledProduct = createProductsWorkflow.runAsStep({ + input: { + products: [ + { + title: "Bundled Clothes", + shipping_profile_id: "sp_123", + variants: [ + { + title: "Bundle", + prices: [ + { + amount: 30, + currency_code: "usd", + }, + ], + options: { + "Default Option": "Default Variant", + }, + inventory_items: inventoryItemIds, + }, + ], + options: [ + { + title: "Default Option", + values: ["Default Variant"], + }, + ], + }, + ], + }, + }).config({ name: "create-bundled-product" }) + } +) +``` + +The bundled product has the same inventory items as those of the products part of the bundle. + +You can now [execute the workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows#3-execute-the-workflow/index.html.md) in [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md), [scheduled jobs](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md), or [subscribers](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md). + + +# Links between Inventory Module and Other Modules + +This document showcases the module links defined between the Inventory Module and other Commerce Modules. + +## Summary + +The Inventory Module has the following links to other modules: + +Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. + +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +| in ||Stored|| +|| in |Read-only|| + +*** + +## Product Module + +Each product variant has different inventory details. Medusa defines a link between the `ProductVariant` and `InventoryItem` data models. + +![A diagram showcasing an example of how data models from the Inventory and Product Module are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709658720/Medusa%20Resources/inventory-product_ejnray.jpg) + +A product variant whose `manage_inventory` property is enabled has an associated inventory item. Through that inventory's items relations in the Inventory Module, you can manage and check the variant's inventory quantity. + +Learn more about product variant's inventory management in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/variant-inventory/index.html.md). + +### Retrieve with Query + +To retrieve the product variants of an inventory item with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variants.*` in `fields`: + +### query.graph + +```ts +const { data: inventoryItems } = await query.graph({ + entity: "inventory_item", + fields: [ + "variants.*", + ], +}) + +// inventoryItems.variants +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: inventoryItems } = useQueryGraphStep({ + entity: "inventory_item", + fields: [ + "variants.*", + ], +}) + +// inventoryItems.variants +``` + +### Manage with Link + +To manage the variants of an inventory item, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.PRODUCT]: { + variant_id: "variant_123", + }, + [Modules.INVENTORY]: { + inventory_item_id: "iitem_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.PRODUCT]: { + variant_id: "variant_123", + }, + [Modules.INVENTORY]: { + inventory_item_id: "iitem_123", + }, +}) +``` + +*** + +## Stock Location Module + +Medusa defines a read-only link between the `InventoryLevel` data model and the [Stock Location Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/index.html.md)'s `StockLocation` data model. This means you can retrieve the details of an inventory level's stock locations, but you don't manage the links in a pivot table in the database. The stock location of an inventory level is determined by the `location_id` property of the `InventoryLevel` data model. + +### Retrieve with Query + +To retrieve the stock locations of an inventory level with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `stock_locations.*` in `fields`: + +### query.graph + +```ts +const { data: inventoryLevels } = await query.graph({ + entity: "inventory_level", + fields: [ + "stock_locations.*", + ], +}) + +// inventoryLevels.stock_locations +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: inventoryLevels } = useQueryGraphStep({ + entity: "inventory_level", + fields: [ + "stock_locations.*", + ], +}) + +// inventoryLevels.stock_locations +``` + + +# 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| +|---|---|---|---| +| in ||Read-only|| + +*** + +## Store Module + +The Store Module has a `Currency` data model that stores the supported currencies of a store. However, these currencies don't hold all the details of a currency, such as its name or symbol. + +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 `Currency` 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.supported_currencies +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: stores } = useQueryGraphStep({ + entity: "store", + fields: [ + "supported_currencies.currency.*", + ], +}) + +// stores.supported_currencies +``` + + +# Customer Accounts + +In this document, you’ll learn how registered and unregistered accounts are distinguished in the Medusa application. + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/customers/index.html.md) to learn how to manage customers using the dashboard. + +## `has_account` Property + +The [Customer data model](https://docs.medusajs.com/references/customer/models/Customer/index.html.md) has a `has_account` property, which is a boolean that indicates whether a customer is registered. + +When a guest customer places an order, a new `Customer` record is created with `has_account` set to `false`. + +When this or another guest customer registers an account with the same email, a new `Customer` record is created with `has_account` set to `true`. + +*** + +## Email Uniqueness + +The above behavior means that two `Customer` records may exist with the same email. However, the main difference is the `has_account` property's value. + +So, there can only be one guest customer (having `has_account=false`) and one registered customer (having `has_account=true`) with the same email. + + +# Links between Customer Module and Other Modules + +This document showcases the module links defined between the Customer Module and other Commerce Modules. + +## Summary + +The Customer Module has the following links to other modules: + +Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. + +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +|| in |Stored|| +| in ||Read-only|| +| in ||Read-only|| + +*** + +## Payment Module + +Medusa defines a link between the `Customer` and `AccountHolder` data models, allowing payment providers to save payment methods for a customer, if the payment provider supports it. + +This link is available starting from Medusa `v2.5.0`. + +### Retrieve with Query + +To retrieve the account holder associated with a customer with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`: + +### query.graph + +```ts +const { data: customers } = await query.graph({ + entity: "customer", + fields: [ + "account_holders.*", + ], +}) + +// customers.account_holders +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: customers } = useQueryGraphStep({ + entity: "customer", + fields: [ + "account_holders.*", + ], +}) + +// customers.account_holders +``` + +### Manage with Link + +To manage the account holders of a customer, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.CUSTOMER]: { + customer_id: "cus_123", + }, + [Modules.PAYMENT]: { + account_holder_id: "acchld_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.CUSTOMER]: { + customer_id: "cus_123", + }, + [Modules.PAYMENT]: { + account_holder_id: "acchld_123", + }, +}) +``` + +*** + +## Cart Module + +Medusa defines a read-only link between the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `Cart` data model and the `Customer` data model. Because the link is read-only from the `Cart`'s side, you can only retrieve the customer of a cart, and not the other way around. + +### Retrieve with Query + +To retrieve the customer of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`: + +### query.graph + +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "customer.*", + ], +}) + +// carts.customer +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "customer.*", + ], +}) + +// carts.customer +``` + +*** + +## Order Module + +Medusa defines a read-only link between the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `Order` data model and the `Customer` data model. Because the link is read-only from the `Order`'s side, you can only retrieve the customer of an order, and not the other way around. + +### Retrieve with Query + +To retrieve the customer of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`: + +### query.graph + +```ts +const { data: orders } = await query.graph({ + entity: "order", + fields: [ + "customer.*", + ], +}) + +// orders.customer +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "customer.*", + ], +}) + +// orders.customer +``` + + +# Cart Concepts + +In this document, you’ll get an overview of the main concepts of a cart. + +## Shipping and Billing Addresses + +A cart has a shipping and billing address. Both of these addresses are represented by the [Address data model](https://docs.medusajs.com/references/cart/models/Address/index.html.md). + +![A diagram showcasing the relation between the Cart and Address data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711532392/Medusa%20Resources/cart-addresses_ls6qmv.jpg) + +*** + +## Line Items + +A line item, represented by the [LineItem](https://docs.medusajs.com/references/cart/models/LineItem/index.html.md) data model, is a quantity of a product variant added to the cart. A cart has multiple line items. + +A line item stores some of the product variant’s properties, such as the `product_title` and `product_description`. It also stores data related to the item’s quantity and price. + +In the Medusa application, a product variant is implemented in the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md). + +*** + +## Shipping Methods + +A shipping method, represented by the [ShippingMethod data model](https://docs.medusajs.com/references/cart/models/ShippingMethod/index.html.md), is used to fulfill the items in the cart after the order is placed. A cart can have more than one shipping method. + +In the Medusa application, the shipping method is created from a shipping option, available through the [Fulfillment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/index.html.md). Its ID is stored in the `shipping_option_id` property of the method. + +### data Property + +After an order is placed, you can use a third-party fulfillment provider to fulfill its shipments. + +If the fulfillment provider requires additional custom data to be passed along from the checkout process, set this data in the `ShippingMethod`'s `data` property. + +The `data` property is an object used to store custom data relevant later for fulfillment. + + +# Links between Cart Module and Other Modules + +This document showcases the module links defined between the Cart Module and other Commerce Modules. + +## Summary + +The Cart Module has the following links to other modules: + +Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. + +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +|| in |Read-only|| +| in ||Stored|| +|| in |Stored|| +|| in |Read-only|| +|| in |Read-only|| +|| in |Stored|| +|| in |Read-only|| +|| in |Read-only|| + +*** + +## Customer Module + +Medusa defines a read-only link between the `Cart` data model and the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md)'s `Customer` data model. This means you can retrieve the details of a cart's customer, but you don't manage the links in a pivot table in the database. The customer of a cart is determined by the `customer_id` property of the `Cart` data model. + +### Retrieve with Query + +To retrieve the customer of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`: + +### query.graph + +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "customer.*", + ], +}) + +// carts.order +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "customer.*", + ], +}) + +// carts.order +``` + +*** + +## Order Module + +The [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md) provides order-management features. + +Medusa defines a link between the `Cart` and `Order` data models. The cart is linked to the order created once the cart is completed. + +![A diagram showcasing an example of how data models from the Cart and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728375735/Medusa%20Resources/cart-order_ijwmfs.jpg) + +### Retrieve with Query + +To retrieve the order of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `order.*` in `fields`: + +### query.graph + +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "order.*", + ], +}) + +// carts.order +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "order.*", + ], +}) + +// carts.order +``` + +### Manage with Link + +To manage the order of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.ORDER]: { + order_id: "order_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.ORDER]: { + order_id: "order_123", + }, +}) +``` + +*** + +## Payment Module + +The [Payment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/index.html.md) handles payment processing and management. + +Medusa defines a link between the `Cart` and `PaymentCollection` data models. A cart has a payment collection which holds all the authorized payment sessions and payments made related to the cart. + +![A diagram showcasing an example of how data models from the Cart and Payment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711537849/Medusa%20Resources/cart-payment_ixziqm.jpg) + +### Retrieve with Query + +To retrieve the payment collection of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `payment_collection.*` in `fields`: + +### query.graph + +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "payment_collection.*", + ], +}) + +// carts.payment_collection +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "payment_collection.*", + ], +}) + +// carts.payment_collection +``` + +### Manage with Link + +To manage the payment collection of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.PAYMENT]: { + payment_collection_id: "paycol_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.PAYMENT]: { + payment_collection_id: "paycol_123", + }, +}) +``` + +*** + +## Product Module + +Medusa defines read-only links between: + +- the `LineItem` data model and the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md)'s `Product` data model. This means you can retrieve the details of a line item's product, but you don't manage the links in a pivot table in the database. The product of a line item is determined by the `product_id` property of the `LineItem` data model. +- the `LineItem` data model and the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md)'s `ProductVariant` data model. This means you can retrieve the details of a line item's variant, but you don't manage the links in a pivot table in the database. The variant of a line item is determined by the `variant_id` property of the `LineItem` data model. + +### Retrieve with Query + +To retrieve the variant of a line item with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variant.*` in `fields`: + +To retrieve the product, pass `product.*` in `fields`. + +### query.graph + +```ts +const { data: lineItems } = await query.graph({ + entity: "line_item", + fields: [ + "variant.*", + ], +}) + +// lineItems.variant +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: lineItems } = useQueryGraphStep({ + entity: "line_item", + fields: [ + "variant.*", + ], +}) + +// lineItems.variant +``` + +*** + +## Promotion Module + +The [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md) provides discount features. + +Medusa defines a link between the `Cart` and `Promotion` data models. This indicates the promotions applied on a cart. + +![A diagram showcasing an example of how data models from the Cart and Promotion modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711538015/Medusa%20Resources/cart-promotion_kuh9vm.jpg) + +Medusa also defines a read-only link between the `LineItemAdjustment` and `Promotion` data models. This means you can retrieve the details of the promotion applied on a line item, but you don't manage the links in a pivot table in the database. The promotion of a line item is determined by the `promotion_id` property of the `LineItemAdjustment` data model. + +### Retrieve with Query + +To retrieve the promotions of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `promotions.*` in `fields`: + +To retrieve the promotion of a line item adjustment, pass `promotion.*` in `fields`. + +### query.graph + +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "promotions.*", + ], +}) + +// carts.promotions +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "promotions.*", + ], +}) + +// carts.promotions +``` + +### Manage with Link + +To manage the promotions of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.PROMOTION]: { + promotion_id: "promo_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.PROMOTION]: { + promotion_id: "promo_123", + }, +}) +``` + +*** + +## Region Module + +Medusa defines a read-only link between the `Cart` data model and the [Region Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/region/index.html.md)'s `Region` data model. This means you can retrieve the details of a cart's region, but you don't manage the links in a pivot table in the database. The region of a cart is determined by the `region_id` property of the `Cart` data model. + +### Retrieve with Query + +To retrieve the region of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `region.*` in `fields`: + +### query.graph + +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "region.*", + ], +}) + +// carts.region +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "region.*", + ], +}) + +// carts.region +``` + +*** + +## Sales Channel Module + +Medusa defines a read-only link between the `Cart` data model and the [Sales Channel Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/index.html.md)'s `SalesChannel` data model. This means you can retrieve the details of a cart's sales channel, but you don't manage the links in a pivot table in the database. The sales channel of a cart is determined by the `sales_channel_id` property of the `Cart` data model. + +### Retrieve with Query + +To retrieve the sales channel of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channel.*` in `fields`: + +### query.graph + +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "sales_channel.*", + ], +}) + +// carts.sales_channel +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "sales_channel.*", + ], +}) + +// carts.sales_channel +``` + + +# Tax Lines in Cart Module + +In this document, you’ll learn about tax lines in a cart and how to retrieve tax lines with the Tax Module. + +## What are Tax Lines? + +A tax line indicates the tax rate of a line item or a shipping method. The [LineItemTaxLine data model](https://docs.medusajs.com/references/cart/models/LineItemTaxLine/index.html.md) represents a line item’s tax line, and the [ShippingMethodTaxLine data model](https://docs.medusajs.com/references/cart/models/ShippingMethodTaxLine/index.html.md) represents a shipping method’s tax line. + +![A diagram showcasing the relation between other data models and the tax line models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711534431/Medusa%20Resources/cart-tax-lines_oheaq6.jpg) + +*** + +## Tax Inclusivity + +By default, the tax amount is calculated by taking the tax rate from the line item or shipping method’s amount, and then adding them to the item/method’s subtotal. + +However, line items and shipping methods have an `is_tax_inclusive` property that, when enabled, indicates that the item or method’s price already includes taxes. + +So, instead of calculating the tax rate and adding it to the item/method’s subtotal, it’s calculated as part of the subtotal. + +The following diagram is a simplified showcase of how a subtotal is calculated from the taxes perspective. + +![A diagram showing an example of calculating the subtotal of a line item using its taxes](https://res.cloudinary.com/dza7lstvk/image/upload/v1711535295/Medusa%20Resources/cart-tax-inclusive_shpr3t.jpg) + +For example, if a line item's amount is `5000`, the tax rate is `10`, and tax inclusivity is enabled, the tax amount is 10% of `5000`, which is `500`, making the unit price of the line item `4500`. + +*** + +## Retrieve Tax Lines + +When using the Cart and Tax modules together, you can use the `getTaxLines` method of the Tax Module’s main service. It retrieves the tax lines for a cart’s line items and shipping methods. + +```ts +// retrieve the cart +const cart = await cartModuleService.retrieveCart("cart_123", { + relations: [ + "items.tax_lines", + "shipping_methods.tax_lines", + "shipping_address", + ], +}) + +// retrieve the tax lines +const taxLines = await taxModuleService.getTaxLines( + [ + ...(cart.items as TaxableItemDTO[]), + ...(cart.shipping_methods as TaxableShippingDTO[]), + ], + { + address: { + ...cart.shipping_address, + country_code: + cart.shipping_address.country_code || "us", + }, + } +) +``` + +Then, use the returned tax lines to set the line items and shipping methods’ tax lines: + +```ts +// set line item tax lines +await cartModuleService.setLineItemTaxLines( + cart.id, + taxLines.filter((line) => "line_item_id" in line) +) + +// set shipping method tax lines +await cartModuleService.setLineItemTaxLines( + cart.id, + taxLines.filter((line) => "shipping_line_id" in line) +) +``` + + +# Promotions Adjustments in Carts + +In this document, you’ll learn how a promotion is applied to a cart’s line items and shipping methods using adjustment lines. + +## What are Adjustment Lines? + +An adjustment line indicates a change to an item or a shipping method’s amount. It’s used to apply promotions or discounts on a cart. + +The [LineItemAdjustment](https://docs.medusajs.com/references/cart/models/LineItemAdjustment/index.html.md) data model represents changes on a line item, and the [ShippingMethodAdjustment](https://docs.medusajs.com/references/cart/models/ShippingMethodAdjustment/index.html.md) data model represents changes on a shipping method. + +![A diagram showcasing the relations between other data models and adjustment line models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711534248/Medusa%20Resources/cart-adjustments_k4sttb.jpg) + +The `amount` property of the adjustment line indicates the amount to be discounted from the original amount. Also, the ID of the applied promotion is stored in the `promotion_id` property of the adjustment line. + +*** + +## Discountable Option + +The [LineItem](https://docs.medusajs.com/references/cart/models/LineItem/index.html.md) data model has an `is_discountable` property that indicates whether promotions can be applied to the line item. It’s enabled by default. + +When disabled, a promotion can’t be applied to a line item. In the context of the Promotion Module, the promotion isn’t applied to the line item even if it matches its rules. + +*** + +## Promotion Actions + +When using the Cart and Promotion modules together, such as in the Medusa application, use the [computeActions method of the Promotion Module’s main service](https://docs.medusajs.com/references/promotion/computeActions/index.html.md). It retrieves the actions of line items and shipping methods. + +Learn more about actions in the [Promotion Module’s documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/actions/index.html.md). + +For example: + +```ts collapsibleLines="1-8" expandButtonLabel="Show Imports" +import { + ComputeActionAdjustmentLine, + ComputeActionItemLine, + ComputeActionShippingLine, + // ... +} from "@medusajs/framework/types" + +// retrieve the cart +const cart = await cartModuleService.retrieveCart("cart_123", { + relations: [ + "items.adjustments", + "shipping_methods.adjustments", + ], +}) + +// retrieve line item adjustments +const lineItemAdjustments: ComputeActionItemLine[] = [] +cart.items.forEach((item) => { + const filteredAdjustments = item.adjustments?.filter( + (adjustment) => adjustment.code !== undefined + ) as unknown as ComputeActionAdjustmentLine[] + if (filteredAdjustments.length) { + lineItemAdjustments.push({ + ...item, + adjustments: filteredAdjustments, + }) + } +}) + +// retrieve shipping method adjustments +const shippingMethodAdjustments: ComputeActionShippingLine[] = + [] +cart.shipping_methods.forEach((shippingMethod) => { + const filteredAdjustments = + shippingMethod.adjustments?.filter( + (adjustment) => adjustment.code !== undefined + ) as unknown as ComputeActionAdjustmentLine[] + if (filteredAdjustments.length) { + shippingMethodAdjustments.push({ + ...shippingMethod, + adjustments: filteredAdjustments, + }) + } +}) + +// compute actions +const actions = await promotionModuleService.computeActions( + ["promo_123"], + { + items: lineItemAdjustments, + shipping_methods: shippingMethodAdjustments, + } +) +``` + +The `computeActions` method accepts the existing adjustments of line items and shipping methods to compute the actions accurately. + +Then, use the returned `addItemAdjustment` and `addShippingMethodAdjustment` actions to set the cart’s line item and the shipping method’s adjustments. + +```ts collapsibleLines="1-8" expandButtonLabel="Show Imports" +import { + AddItemAdjustmentAction, + AddShippingMethodAdjustment, + // ... +} from "@medusajs/framework/types" + +// ... + +await cartModuleService.setLineItemAdjustments( + cart.id, + actions.filter( + (action) => action.action === "addItemAdjustment" + ) as AddItemAdjustmentAction[] +) + +await cartModuleService.setShippingMethodAdjustments( + cart.id, + actions.filter( + (action) => + action.action === "addShippingMethodAdjustment" + ) as AddShippingMethodAdjustment[] +) +``` + + +# Order Concepts + +In this document, you’ll learn about orders and related concepts + +## Order Items + +The items purchased in the order are represented by the [OrderItem data model](https://docs.medusajs.com/references/order/models/OrderItem/index.html.md). An order can have multiple items. + +![A diagram showcasing the relation between an order and its items.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712304722/Medusa%20Resources/order-order-items_uvckxd.jpg) + +### Item’s Product Details + +The details of the purchased products are represented by the [LineItem data model](https://docs.medusajs.com/references/order/models/OrderLineItem/index.html.md). Not only does a line item hold the details of the product, but also details related to its price, adjustments due to promotions, and taxes. + +*** + +## Order’s Shipping Method + +An order has one or more shipping methods used to handle item shipment. + +Each shipping method is represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md) that holds its details. The shipping method is linked to the order through the [OrderShipping data model](https://docs.medusajs.com/references/order/models/OrderShipping/index.html.md). + +![A diagram showcasing the relation between an order and its items.](https://res.cloudinary.com/dza7lstvk/image/upload/v1719570409/Medusa%20Resources/order-shipping-method_tkggvd.jpg) + +### data Property + +When fulfilling the order, you can use a third-party fulfillment provider that requires additional custom data to be passed along from the order creation process. + +The `OrderShippingMethod` data model has a `data` property. It’s an object used to store custom data relevant later for fulfillment. + +The Medusa application passes the `data` property to the Fulfillment Module when fulfilling items. + +*** + +## Order Totals + +The order’s total amounts (including tax total, total after an item is returned, etc…) are represented by the [OrderSummary data model](https://docs.medusajs.com/references/order/models/OrderSummary/index.html.md). + +*** + +## Order Payments + +Payments made on an order, whether they’re capture or refund payments, are recorded as transactions represented by the [OrderTransaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md). + +An order can have multiple transactions. The sum of these transactions must be equal to the order summary’s total. Otherwise, there’s an outstanding amount. + +Learn more about transactions in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/transactions/index.html.md). + + # Order Claim In this document, you’ll learn about order claims. @@ -24373,55 +24424,6 @@ Once the Order Edit is confirmed, any additional payment or refund required can This is determined by the comparison between the `OrderSummary` and the order's transactions, as mentioned in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/transactions#checking-outstanding-amount/index.html.md). -# Order Concepts - -In this document, you’ll learn about orders and related concepts - -## Order Items - -The items purchased in the order are represented by the [OrderItem data model](https://docs.medusajs.com/references/order/models/OrderItem/index.html.md). An order can have multiple items. - -![A diagram showcasing the relation between an order and its items.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712304722/Medusa%20Resources/order-order-items_uvckxd.jpg) - -### Item’s Product Details - -The details of the purchased products are represented by the [LineItem data model](https://docs.medusajs.com/references/order/models/OrderLineItem/index.html.md). Not only does a line item hold the details of the product, but also details related to its price, adjustments due to promotions, and taxes. - -*** - -## Order’s Shipping Method - -An order has one or more shipping methods used to handle item shipment. - -Each shipping method is represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md) that holds its details. The shipping method is linked to the order through the [OrderShipping data model](https://docs.medusajs.com/references/order/models/OrderShipping/index.html.md). - -![A diagram showcasing the relation between an order and its items.](https://res.cloudinary.com/dza7lstvk/image/upload/v1719570409/Medusa%20Resources/order-shipping-method_tkggvd.jpg) - -### data Property - -When fulfilling the order, you can use a third-party fulfillment provider that requires additional custom data to be passed along from the order creation process. - -The `OrderShippingMethod` data model has a `data` property. It’s an object used to store custom data relevant later for fulfillment. - -The Medusa application passes the `data` property to the Fulfillment Module when fulfilling items. - -*** - -## Order Totals - -The order’s total amounts (including tax total, total after an item is returned, etc…) are represented by the [OrderSummary data model](https://docs.medusajs.com/references/order/models/OrderSummary/index.html.md). - -*** - -## Order Payments - -Payments made on an order, whether they’re capture or refund payments, are recorded as transactions represented by the [OrderTransaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md). - -An order can have multiple transactions. The sum of these transactions must be equal to the order summary’s total. Otherwise, there’s an outstanding amount. - -Learn more about transactions in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/transactions/index.html.md). - - # Order Exchange In this document, you’ll learn about order exchanges. @@ -25029,6 +25031,45 @@ 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 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. @@ -25212,45 +25253,6 @@ The order’s version is incremented when: 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\`| - - # Tax Lines in Order Module In this document, you’ll learn about tax lines in an order. @@ -25328,6 +25330,110 @@ 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`. +# Account Holders and Saved Payment Methods + +In this documentation, you'll learn about account holders, and how they're used to save payment methods in third-party payment providers. + +Account holders are available starting from Medusa `v2.5.0`. + +## What's an Account Holder? + +An account holder represents a customer that can have saved payment methods in a third-party service. It's represented by the `AccountHolder` data model. + +It holds fields retrieved from the third-party provider, such as: + +- `external_id`: The ID of the equivalent customer or account holder in the third-party provider. +- `data`: Data returned by the payment provider when the account holder is created. + +A payment provider that supports saving payment methods for customers would create the equivalent of an account holder in the third-party provider. Then, whenever a payment method is saved, it would be saved under the account holder in the third-party provider. + +### Relation between Account Holder and Customer + +The Medusa application creates a link between the [Customer](https://docs.medusajs.com/references/customer/models/Customer/index.html.md) data model of the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md) and the `AccountHolder` data model of the Payment Module. + +This link indicates that a customer can have more than one account holder, each representing saved payment methods in different payment providers. + +Learn more about this link in the [Link to Other Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/links-to-other-modules/index.html.md) guide. + +*** + +## Save Payment Methods + +If a payment provider supports saving payment methods for a customer, they must implement the following methods: + +- `createAccountHolder`: Creates an account holder in the payment provider. The Payment Module uses this method before creating the account holder in Medusa, and uses the returned data to set fields like `external_id` and `data` in the created `AccountHolder` record. +- `deleteAccountHolder`: Deletes an account holder in the payment provider. The Payment Module uses this method when an account holder is deleted in Medusa. +- `savePaymentMethod`: Saves a payment method for an account holder in the payment provider. +- `listPaymentMethods`: Lists saved payment methods in the third-party service for an account holder. This is useful when displaying the customer's saved payment methods in the storefront. + +Learn more about implementing these methods in the [Create Payment Provider guide](https://docs.medusajs.com/references/payment/provider/index.html.md). + +*** + +## Account Holder in Medusa Payment Flows + +In the Medusa application, when a payment session is created for a registered customer, the Medusa application uses the Payment Module to create an account holder for the customer. + +Consequently, the Payment Module uses the payment provider to create an account holder in the third-party service, then creates the account holder in Medusa. + +This flow is only supported if the chosen payment provider has implemented the necessary [save payment methods](#save-payment-methods). + + +# Payment Module Options + +In this document, you'll learn about the options of the Payment Module. + +## All Module Options + +|Option|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`webhook\_delay\`|A number indicating the delay in milliseconds before processing a webhook event.|No|\`5000\`| +|\`webhook\_retries\`|The number of times to retry the webhook event processing in case of an error.|No|\`3\`| +|\`providers\`|An array of payment providers to install and register. Learn more |No|-| + +*** + +## providers Option + +The `providers` option is an array of payment module providers. + +When the Medusa application starts, these providers are registered and can be used to process payments. + +For example: + +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" + +// ... + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/payment", + options: { + providers: [ + { + resolve: "@medusajs/medusa/payment-stripe", + id: "stripe", + options: { + // ... + }, + }, + ], + }, + }, + ], +}) +``` + +The `providers` option is an array of objects that accept the following properties: + +- `resolve`: A string indicating the package name of the module provider or the path to it relative to the `src` directory. +- `id`: A string indicating the provider's unique name or ID. +- `options`: An optional object of the module provider's options. + + # Links between Payment Module and Other Modules This document showcases the module links defined between the Payment Module and other Commerce Modules. @@ -25674,110 +25780,6 @@ createRemoteLinkStep({ ``` -# Account Holders and Saved Payment Methods - -In this documentation, you'll learn about account holders, and how they're used to save payment methods in third-party payment providers. - -Account holders are available starting from Medusa `v2.5.0`. - -## What's an Account Holder? - -An account holder represents a customer that can have saved payment methods in a third-party service. It's represented by the `AccountHolder` data model. - -It holds fields retrieved from the third-party provider, such as: - -- `external_id`: The ID of the equivalent customer or account holder in the third-party provider. -- `data`: Data returned by the payment provider when the account holder is created. - -A payment provider that supports saving payment methods for customers would create the equivalent of an account holder in the third-party provider. Then, whenever a payment method is saved, it would be saved under the account holder in the third-party provider. - -### Relation between Account Holder and Customer - -The Medusa application creates a link between the [Customer](https://docs.medusajs.com/references/customer/models/Customer/index.html.md) data model of the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md) and the `AccountHolder` data model of the Payment Module. - -This link indicates that a customer can have more than one account holder, each representing saved payment methods in different payment providers. - -Learn more about this link in the [Link to Other Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/links-to-other-modules/index.html.md) guide. - -*** - -## Save Payment Methods - -If a payment provider supports saving payment methods for a customer, they must implement the following methods: - -- `createAccountHolder`: Creates an account holder in the payment provider. The Payment Module uses this method before creating the account holder in Medusa, and uses the returned data to set fields like `external_id` and `data` in the created `AccountHolder` record. -- `deleteAccountHolder`: Deletes an account holder in the payment provider. The Payment Module uses this method when an account holder is deleted in Medusa. -- `savePaymentMethod`: Saves a payment method for an account holder in the payment provider. -- `listPaymentMethods`: Lists saved payment methods in the third-party service for an account holder. This is useful when displaying the customer's saved payment methods in the storefront. - -Learn more about implementing these methods in the [Create Payment Provider guide](https://docs.medusajs.com/references/payment/provider/index.html.md). - -*** - -## Account Holder in Medusa Payment Flows - -In the Medusa application, when a payment session is created for a registered customer, the Medusa application uses the Payment Module to create an account holder for the customer. - -Consequently, the Payment Module uses the payment provider to create an account holder in the third-party service, then creates the account holder in Medusa. - -This flow is only supported if the chosen payment provider has implemented the necessary [save payment methods](#save-payment-methods). - - -# Payment Module Options - -In this document, you'll learn about the options of the Payment Module. - -## All Module Options - -|Option|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`webhook\_delay\`|A number indicating the delay in milliseconds before processing a webhook event.|No|\`5000\`| -|\`webhook\_retries\`|The number of times to retry the webhook event processing in case of an error.|No|\`3\`| -|\`providers\`|An array of payment providers to install and register. Learn more |No|-| - -*** - -## providers Option - -The `providers` option is an array of payment module providers. - -When the Medusa application starts, these providers are registered and can be used to process payments. - -For example: - -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" - -// ... - -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/payment", - options: { - providers: [ - { - resolve: "@medusajs/medusa/payment-stripe", - id: "stripe", - options: { - // ... - }, - }, - ], - }, - }, - ], -}) -``` - -The `providers` option is an array of objects that accept the following properties: - -- `resolve`: A string indicating the package name of the module provider or the path to it relative to the `src` directory. -- `id`: A string indicating the provider's unique name or ID. -- `options`: An optional object of the module provider's options. - - # Payment In this document, you’ll learn what a payment is and how it's created, captured, and refunded. @@ -25850,6 +25852,57 @@ It also implements the payment flow during checkout as explained in [this docume ![Diagram showcasing the relation between the Payment and Cart modules](https://res.cloudinary.com/dza7lstvk/image/upload/v1711537849/Medusa%20Resources/cart-payment_ixziqm.jpg) +# Payment Module Provider + +In this document, you’ll learn what a payment module provider is. + +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's a Payment Module Provider? + +A payment module provider registers a payment provider that handles payment processing in the Medusa application. It integrates third-party payment providers, such as Stripe. + +To authorize a payment amount with a payment provider, a payment session is created and associated with that payment provider. The payment provider is then used to handle the authorization. + +After the payment session is authorized, the payment provider is associated with the resulting payment and handles its payment processing, such as to capture or refund payment. + +### List of Payment Module Providers + +- [Stripe](https://docs.medusajs.com/commerce-modules/payment/payment-provider/stripe/index.html.md) + +*** + +## System Payment Provider + +The Payment Module provides a `system` payment provider that acts as a placeholder payment provider. + +It doesn’t handle payment processing and delegates that to the merchant. It acts similarly to a cash-on-delivery (COD) payment method. + +*** + +## How are Payment Providers Created? + +A payment provider is a module whose main service extends the `AbstractPaymentProvider` imported from `@medusajs/framework/utils`. + +Refer to [this guide](https://docs.medusajs.com/references/payment/provider/index.html.md) on how to create a payment provider for the Payment Module. + +*** + +## Configure Payment Providers + +The Payment Module accepts a `providers` option that allows you to register providers in your application. + +Learn more about this option in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/module-options#providers/index.html.md). + +*** + +## PaymentProvider Data Model + +When the Medusa application starts and registers the payment providers, it also creates a record of the `PaymentProvider` data model if none exists. + +This data model is used to reference a payment provider and determine whether it’s installed in the application. + + # Accept Payment Flow In this document, you’ll learn how to implement an accept-payment flow using workflows or the Payment Module's main service. @@ -26017,6 +26070,39 @@ You can then: Some payment providers allow capturing the payment automatically once it’s authorized. In that case, you don’t need to do it manually. +# Payment Session + +In this document, you’ll learn what a payment session is. + +## What's a Payment Session? + +A payment session, represented by the [PaymentSession data model](https://docs.medusajs.com/references/payment/models/PaymentSession/index.html.md), is a payment amount to be authorized. It’s associated with a payment provider that handles authorizing it. + +A payment collection can have multiple payment sessions. Using this feature, you can implement payment in installments or payments using multiple providers. + +![Diagram showcasing how every payment session has a different payment provider](https://res.cloudinary.com/dza7lstvk/image/upload/v1711565056/Medusa%20Resources/payment-session-provider_guxzqt.jpg) + +*** + +## data Property + +Payment providers may need additional data to process the payment later. The `PaymentSession` data model has a `data` property used to store that data. + +For example, the customer's ID in Stripe is stored in the `data` property. + +*** + +## Payment Session Status + +The `status` property of a payment session indicates its current status. Its value can be: + +- `pending`: The payment session is awaiting authorization. +- `requires_more`: The payment session requires an action before it’s authorized. For example, to enter a 3DS code. +- `authorized`: The payment session is authorized. +- `error`: An error occurred while authorizing the payment. +- `canceled`: The authorization of the payment session has been canceled. + + # Webhook Events In this document, you’ll learn how the Payment Module supports listening to webhook events. @@ -26053,1606 +26139,6 @@ If the event's details indicate that the payment should be captured, then the [c After the payment webhook actions are processed and the payment is authorized or captured, the Medusa application completes the cart associated with the payment's collection if it's not completed yet. -# Payment Module Provider - -In this document, you’ll learn what a payment module provider is. - -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's a Payment Module Provider? - -A payment module provider registers a payment provider that handles payment processing in the Medusa application. It integrates third-party payment providers, such as Stripe. - -To authorize a payment amount with a payment provider, a payment session is created and associated with that payment provider. The payment provider is then used to handle the authorization. - -After the payment session is authorized, the payment provider is associated with the resulting payment and handles its payment processing, such as to capture or refund payment. - -### List of Payment Module Providers - -- [Stripe](https://docs.medusajs.com/commerce-modules/payment/payment-provider/stripe/index.html.md) - -*** - -## System Payment Provider - -The Payment Module provides a `system` payment provider that acts as a placeholder payment provider. - -It doesn’t handle payment processing and delegates that to the merchant. It acts similarly to a cash-on-delivery (COD) payment method. - -*** - -## How are Payment Providers Created? - -A payment provider is a module whose main service extends the `AbstractPaymentProvider` imported from `@medusajs/framework/utils`. - -Refer to [this guide](https://docs.medusajs.com/references/payment/provider/index.html.md) on how to create a payment provider for the Payment Module. - -*** - -## Configure Payment Providers - -The Payment Module accepts a `providers` option that allows you to register providers in your application. - -Learn more about this option in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/module-options#providers/index.html.md). - -*** - -## PaymentProvider Data Model - -When the Medusa application starts and registers the payment providers, it also creates a record of the `PaymentProvider` data model if none exists. - -This data model is used to reference a payment provider and determine whether it’s installed in the application. - - -# Payment Session - -In this document, you’ll learn what a payment session is. - -## What's a Payment Session? - -A payment session, represented by the [PaymentSession data model](https://docs.medusajs.com/references/payment/models/PaymentSession/index.html.md), is a payment amount to be authorized. It’s associated with a payment provider that handles authorizing it. - -A payment collection can have multiple payment sessions. Using this feature, you can implement payment in installments or payments using multiple providers. - -![Diagram showcasing how every payment session has a different payment provider](https://res.cloudinary.com/dza7lstvk/image/upload/v1711565056/Medusa%20Resources/payment-session-provider_guxzqt.jpg) - -*** - -## data Property - -Payment providers may need additional data to process the payment later. The `PaymentSession` data model has a `data` property used to store that data. - -For example, the customer's ID in Stripe is stored in the `data` property. - -*** - -## Payment Session Status - -The `status` property of a payment session indicates its current status. Its value can be: - -- `pending`: The payment session is awaiting authorization. -- `requires_more`: The payment session requires an action before it’s authorized. For example, to enter a 3DS code. -- `authorized`: The payment session is authorized. -- `error`: An error occurred while authorizing the payment. -- `canceled`: The authorization of the payment session has been canceled. - - -# 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). - -## computeActions Method - -The Promotion Module's main service has a [computeActions method](https://docs.medusajs.com/references/promotion/computeActions/index.html.md) that returns an array of actions to perform on a cart when one or more promotions are applied. - -Actions inform you what adjustment must be made to a cart item or shipping method. Each action is an object having the `action` property indicating the type of action. - -*** - -## Action Types - -### `addItemAdjustment` Action - -The `addItemAdjustment` action indicates that an adjustment must be made to an item. For example, removing $5 off its amount. - -This action has the following format: - -```ts -export interface AddItemAdjustmentAction { - action: "addItemAdjustment" - item_id: string - amount: number - code: string - description?: string -} -``` - -This action means that a new record should be created of the `LineItemAdjustment` data model in the Cart Module, or `OrderLineItemAdjustment` data model in the Order Module. - -Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.AddItemAdjustmentAction/index.html.md) for details on the object’s properties. - -### `removeItemAdjustment` Action - -The `removeItemAdjustment` action indicates that an adjustment must be removed from a line item. For example, remove the $5 discount. - -The `computeActions` method accepts any previous item adjustments in the `items` property of the second parameter. - -This action has the following format: - -```ts -export interface RemoveItemAdjustmentAction { - action: "removeItemAdjustment" - adjustment_id: string - description?: string - code: string -} -``` - -This action means that a new record should be removed of the `LineItemAdjustment` (or `OrderLineItemAdjustment`) with the specified ID in the `adjustment_id` property. - -Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.RemoveItemAdjustmentAction/index.html.md) for details on the object’s properties. - -### `addShippingMethodAdjustment` Action - -The `addShippingMethodAdjustment` action indicates that an adjustment must be made on a shipping method. For example, make the shipping method free. - -This action has the following format: - -```ts -export interface AddShippingMethodAdjustment { - action: "addShippingMethodAdjustment" - shipping_method_id: string - amount: number - code: string - description?: string -} -``` - -This action means that a new record should be created of the `ShippingMethodAdjustment` data model in the Cart Module, or `OrderShippingMethodAdjustment` data model in the Order Module. - -Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.AddShippingMethodAdjustment/index.html.md) for details on the object’s properties. - -### `removeShippingMethodAdjustment` Action - -The `removeShippingMethodAdjustment` action indicates that an adjustment must be removed from a shipping method. For example, remove the free shipping discount. - -The `computeActions` method accepts any previous shipping method adjustments in the `shipping_methods` property of the second parameter. - -This action has the following format: - -```ts -export interface RemoveShippingMethodAdjustment { - action: "removeShippingMethodAdjustment" - adjustment_id: string - code: string -} -``` - -When the Medusa application receives this action type, it removes the `ShippingMethodAdjustment` (or `OrderShippingMethodAdjustment`) with the specified ID in the `adjustment_id` property. - -Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.RemoveShippingMethodAdjustment/index.html.md) for details on the object’s properties. - -### `campaignBudgetExceeded` Action - -When the `campaignBudgetExceeded` action is returned, the promotions within a campaign can no longer be used as the campaign budget has been exceeded. - -This action has the following format: - -```ts -export interface CampaignBudgetExceededAction { - action: "campaignBudgetExceeded" - code: string -} -``` - -Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.CampaignBudgetExceededAction/index.html.md) for details on the object’s properties. - - -# 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| -|---|---|---|---| -| in ||Read-only|| -| in ||Read-only|| -|| in |Stored|| - -*** - -## 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.region -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "region.*", - ], -}) - -// carts.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.region -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: orders } = useQueryGraphStep({ - entity: "order", - fields: [ - "region.*", - ], -}) - -// orders.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.payment_providers -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: regions } = useQueryGraphStep({ - entity: "region", - fields: [ - "payment_providers.*", - ], -}) - -// regions.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", - }, -}) -``` - - -# Promotion Concepts - -In this document, you’ll learn about the main promotion and rule concepts in the Promotion Module. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/promotions/index.html.md) to learn how to manage promotions using the dashboard. - -## What is a Promotion? - -A promotion, represented by the [Promotion data model](https://docs.medusajs.com/references/promotion/models/Promotion/index.html.md), is a discount that can be applied on cart items, shipping methods, or entire orders. - -A promotion has two types: - -- `standard`: A standard promotion with rules. -- `buyget`: “A buy X get Y” promotion with rules. - -|\`standard\`|\`buyget\`| -|---|---| -|A coupon code that gives customers 10% off their entire order.|Buy two shirts and get another for free.| -|A coupon code that gives customers $15 off any shirt in their order.|Buy two shirts and get 10% off the entire order.| -|A discount applied automatically for VIP customers that removes 10% off their shipping method’s amount.|Spend $100 and get free shipping.| - -The Medusa Admin UI may not provide a way to create each of these promotion examples. However, they are supported by the Promotion Module and Medusa's workflows and API routes. - -*** - -## PromotionRule - -A promotion can be restricted by a set of rules, each rule is represented by the [PromotionRule data model](https://docs.medusajs.com/references/promotion/models/PromotionRule/index.html.md). - -For example, you can create a promotion that only customers of the `VIP` customer group can use. - -![A diagram showcasing the relation between Promotion and PromotionRule](https://res.cloudinary.com/dza7lstvk/image/upload/v1709833196/Medusa%20Resources/promotion-promotion-rule_msbx0w.jpg) - -A `PromotionRule`'s `attribute` property indicates the property's name to which this rule is applied. - -For example, `customer_group_id`. Its value is stored in the `PromotionRuleValue` data model. So, a rule can have multiple values. - -When testing whether a promotion can be applied to a cart, the rule's `attribute` property and its values are tested on the cart itself. - -For example, the cart's customer must be part of the customer group(s) indicated in the promotion rule's value. - -*** - -## Flexible Rules - -The `PromotionRule`'s `operator` property adds more flexibility to the rule’s condition rather than simple equality (`eq`). - -For example, to restrict the promotion to only `VIP` and `B2B` customer groups: - -- Add a `PromotionRule` record with its `attribute` property set to `customer_group_id` and `operator` property to `in`. -- Add two `PromotionRuleValue` records associated with the rule: one with the value `VIP` and the other `B2B`. - -![A diagram showcasing the relation between PromotionRule and PromotionRuleValue when a rule has multiple values](https://res.cloudinary.com/dza7lstvk/image/upload/v1709897383/Medusa%20Resources/promotion-promotion-rule-multiple_hctpmt.jpg) - -In this case, a customer’s group must be in the `VIP` and `B2B` set of values to use the promotion. - - -# Application Method - -In this document, you'll learn what an application method is. - -## What is an Application Method? - -The [ApplicationMethod data model](https://docs.medusajs.com/references/promotion/models/ApplicationMethod/index.html.md) defines how a promotion is applied: - -|Property|Purpose| -|---|---| -|\`type\`|Does the promotion discount a fixed amount or a percentage?| -|\`target\_type\`|Is the promotion applied on a cart item, shipping method, or the entire order?| -|\`allocation\`|Is the discounted amount applied on each item or split between the applicable items?| - -## Target Promotion Rules - -When the promotion is applied to a cart item or a shipping method, you can restrict which items/shipping methods the promotion is applied to. - -The `ApplicationMethod` data model has a collection of `PromotionRule` records to restrict which items or shipping methods the promotion applies to. The `target_rules` property represents this relation. - -![A diagram showcasing the target\_rules relation between the ApplicationMethod and PromotionRule data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709898273/Medusa%20Resources/application-method-target-rules_hqaymz.jpg) - -In this example, the promotion is only applied on products in the cart having the SKU `SHIRT`. - -*** - -## Buy Promotion Rules - -When the promotion’s type is `buyget`, you must specify the “buy X” condition. For example, a cart must have two shirts before the promotion can be applied. - -The application method has a collection of `PromotionRule` items to define the “buy X” rule. The `buy_rules` property represents this relation. - -![A diagram showcasing the buy\_rules relation between the ApplicationMethod and PromotionRule data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709898453/Medusa%20Resources/application-method-buy-rules_djjuhw.jpg) - -In this example, the cart must have two products with the SKU `SHIRT` for the promotion to be applied. - - -# Links between Promotion Module and Other Modules - -This document showcases the module links defined between the Promotion Module and other Commerce Modules. - -## Summary - -The Promotion Module has the following links to other modules: - -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. - -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -| in ||Stored|| -| in ||Read-only|| -| in ||Stored|| - -*** - -## Cart Module - -A promotion can be applied on line items and shipping methods of a cart. Medusa defines a link between the `Cart` and `Promotion` data models. - -![A diagram showcasing an example of how data models from the Cart and Promotion modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711538015/Medusa%20Resources/cart-promotion_kuh9vm.jpg) - -Medusa also defines a read-only link between the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `LineItemAdjustment` data model and the `Promotion` data model. Because the link is read-only from the `LineItemAdjustment`'s side, you can only retrieve the promotion applied on a line item, and not the other way around. - -### Retrieve with Query - -To retrieve the carts that a promotion is applied on with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `carts.*` in `fields`: - -To retrieve the promotion of a line item adjustment, pass `promotion.*` in `fields`. - -### query.graph - -```ts -const { data: promotions } = await query.graph({ - entity: "promotion", - fields: [ - "carts.*", - ], -}) - -// promotions.carts -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: promotions } = useQueryGraphStep({ - entity: "promotion", - fields: [ - "carts.*", - ], -}) - -// promotions.carts -``` - -### Manage with Link - -To manage the promotions of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", - }, -}) -``` - -*** - -## Order Module - -An order is associated with the promotion applied on it. Medusa defines a link between the `Order` and `Promotion` data models. - -![A diagram showcasing an example of how data models from the Order and Promotion modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716555015/Medusa%20Resources/order-promotion_dgjzzd.jpg) - -### Retrieve with Query - -To retrieve the orders a promotion is applied on with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `orders.*` in `fields`: - -### query.graph - -```ts -const { data: promotions } = await query.graph({ - entity: "promotion", - fields: [ - "orders.*", - ], -}) - -// promotions.orders -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: promotions } = useQueryGraphStep({ - entity: "promotion", - fields: [ - "orders.*", - ], -}) - -// promotions.orders -``` - -### Manage with Link - -To manage the promotion of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.ORDER]: { - order_id: "order_123", - }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.ORDER]: { - order_id: "order_123", - }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", - }, -}) -``` - - -# Campaign - -In this document, you'll learn about campaigns. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/promotions/campaigns/index.html.md) to learn how to manage campaigns using the dashboard. - -## What is a Campaign? - -A [Campaign](https://docs.medusajs.com/references/promotion/models/Campaign/index.html.md) combines promotions under the same conditions, such as start and end dates. - -![A diagram showcasing the relation between the Campaign and Promotion data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709899225/Medusa%20Resources/campagin-promotion_hh3qsi.jpg) - -*** - -## Campaign Limits - -Each campaign has a budget represented by the [CampaignBudget data model](https://docs.medusajs.com/references/promotion/models/CampaignBudget/index.html.md). The budget limits how many times the promotion can be used. - -There are two types of budgets: - -- `spend`: An amount that, when crossed, the promotion becomes unusable. For example, if the amount limit is set to `$100`, and the total amount of usage of this promotion crosses that threshold, the promotion can no longer be applied. -- `usage`: The number of times that a promotion can be used. For example, if the usage limit is set to `10`, the promotion can be used only 10 times by customers. After that, it can no longer be applied. - -![A diagram showcasing the relation between the Campaign and CampaignBudget data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709899463/Medusa%20Resources/campagin-budget_rvqlmi.jpg) - - -# Pricing Concepts - -In this document, you’ll learn about the main concepts in the Pricing Module. - -## Price Set - -A [PriceSet](https://docs.medusajs.com/references/pricing/models/PriceSet/index.html.md) represents a collection of prices that are linked to a resource (for example, a product or a shipping option). - -Each of these prices are represented by the [Price data module](https://docs.medusajs.com/references/pricing/models/Price/index.html.md). - -![A diagram showcasing the relation between the price set and price](https://res.cloudinary.com/dza7lstvk/image/upload/v1709648650/Medusa%20Resources/price-set-money-amount_xeees0.jpg) - -*** - -## Price List - -A [PriceList](https://docs.medusajs.com/references/pricing/models/PriceList/index.html.md) is a group of prices only enabled if their conditions and rules are satisfied. - -A price list has optional `start_date` and `end_date` properties that indicate the date range in which a price list can be applied. - -Its associated prices are represented by the `Price` data model. - - -# Links between Pricing Module and Other Modules - -This document showcases the module links defined between the Pricing Module and other Commerce Modules. - -## Summary - -The Pricing Module has the following links to other modules: - -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -| in ||Stored|| -| in ||Stored|| - -*** - -## 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.shipping_option -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: priceSets } = useQueryGraphStep({ - entity: "price_set", - fields: [ - "shipping_option.*", - ], -}) - -// priceSets.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.variant -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: priceSets } = useQueryGraphStep({ - entity: "price_set", - fields: [ - "variant.*", - ], -}) - -// priceSets.variant -``` - -### Manage with Link - -To manage the price set of a variant, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.PRODUCT]: { - variant_id: "variant_123", - }, - [Modules.PRICING]: { - price_set_id: "pset_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.PRODUCT]: { - variant_id: "variant_123", - }, - [Modules.PRICING]: { - price_set_id: "pset_123", - }, -}) -``` - - -# Prices Calculation - -In this document, you'll learn how prices are calculated when you use the [calculatePrices method](https://docs.medusajs.com/references/pricing/calculatePrices/index.html.md) of the Pricing Module's main service. - -## calculatePrices Method - -The [calculatePrices method](https://docs.medusajs.com/references/pricing/calculatePrices/index.html.md) accepts as parameters the ID of one or more price sets and a context. - -It returns a price object with the best matching price for each price set. - -### Calculation Context - -The calculation context is an optional object passed as a second parameter to the `calculatePrices` method. It accepts rules to restrict the selected prices in the price set. - -For example: - -```ts -const price = await pricingModuleService.calculatePrices( - { id: [priceSetId] }, - { - context: { - currency_code: currencyCode, - region_id: "reg_123", - }, - } -) -``` - -In this example, you retrieve the prices in a price set for the specified currency code and region ID. - -### Returned Price Object - -For each price set, the `calculatePrices` method selects two prices: - -- A calculated price: Either a price that belongs to a price list and best matches the specified context, or the same as the original price. -- An original price, which is either: - - The same price as the calculated price if the price list it belongs to is of type `override`; - - Or a price that doesn't belong to a price list and best matches the specified context. - -Both prices are returned in an object that has the following properties: - -- id: (\`string\`) The ID of the price set from which the price was selected. -- is\_calculated\_price\_price\_list: (\`boolean\`) Whether the calculated price belongs to a price list. -- calculated\_amount: (\`number\`) The amount of the calculated price, or \`null\` if there isn't a calculated price. This is the amount shown to the customer. -- is\_original\_price\_price\_list: (\`boolean\`) Whether the original price belongs to a price list. -- original\_amount: (\`number\`) The amount of the original price, or \`null\` if there isn't an original price. This amount is useful to compare with the \`calculated\_amount\`, such as to check for discounted value. -- currency\_code: (\`string\`) The currency code of the calculated price, or \`null\` if there isn't a calculated price. -- is\_calculated\_price\_tax\_inclusive: (\`boolean\`) Whether the calculated price is tax inclusive. Learn more about tax-inclusivity in \[this document]\(../tax-inclusive-pricing/page.mdx) -- is\_original\_price\_tax\_inclusive: (\`boolean\`) Whether the original price is tax inclusive. Learn more about tax-inclusivity in \[this document]\(../tax-inclusive-pricing/page.mdx) -- calculated\_price: (\`object\`) The calculated price's price details. - - - id: (\`string\`) The ID of the price. - - - price\_list\_id: (\`string\`) The ID of the associated price list. - - - price\_list\_type: (\`string\`) The price list's type. For example, \`sale\`. - - - min\_quantity: (\`number\`) The price's min quantity condition. - - - max\_quantity: (\`number\`) The price's max quantity condition. -- original\_price: (\`object\`) The original price's price details. - - - id: (\`string\`) The ID of the price. - - - price\_list\_id: (\`string\`) The ID of the associated price list. - - - price\_list\_type: (\`string\`) The price list's type. For example, \`sale\`. - - - min\_quantity: (\`number\`) The price's min quantity condition. - - - max\_quantity: (\`number\`) The price's max quantity condition. - -*** - -## Examples - -Consider the following price set: - -```ts -const priceSet = await pricingModuleService.createPriceSets({ - prices: [ - // default price - { - amount: 500, - currency_code: "EUR", - rules: {}, - }, - // prices with rules - { - amount: 400, - currency_code: "EUR", - rules: { - region_id: "reg_123", - }, - }, - { - amount: 450, - currency_code: "EUR", - rules: { - city: "krakow", - }, - }, - { - amount: 500, - currency_code: "EUR", - rules: { - city: "warsaw", - region_id: "reg_123", - }, - }, - ], -}) -``` - -### 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 - -### Price Selection with Price List - -### Code - -```ts -const priceList = pricingModuleService.createPriceLists([{ - title: "Summer Price List", - description: "Price list for summer sale", - starts_at: Date.parse("01/10/2023").toString(), - ends_at: Date.parse("31/10/2023").toString(), - rules: { - region_id: ['PL'] - }, - type: "sale", - prices: [ - { - amount: 400, - currency_code: "EUR", - price_set_id: priceSet.id, - }, - { - amount: 450, - currency_code: "EUR", - price_set_id: priceSet.id, - }, - ], -}]); - -const price = await pricingModuleService.calculatePrices( - { id: [priceSet.id] }, - { - context: { - currency_code: "EUR", - region_id: "PL", - city: "krakow" - } - } -) -``` - -### Result - - -# Price Rules - -In this document, you'll learn about price rules for price sets and price lists. - -## Price Rule - -You can restrict prices by rules. Each rule of a price is represented by the [PriceRule data model](https://docs.medusajs.com/references/pricing/models/PriceRule/index.html.md). - -The `Price` data model has a `rules_count` property, which indicates how many rules, represented by `PriceRule`, are applied to the price. - -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) - - -# 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 - - -# Publishable API Keys with Sales Channels - -In this document, you’ll learn what publishable API keys are and how to use them with sales channels. - -## Publishable API Keys with Sales Channels - -A publishable API key, provided by the API Key Module, is a client key scoped to one or more sales channels. - -When sending a request to a Store API route, you must pass a publishable API key in the header of the request: - -```bash -curl http://localhost:9000/store/products \ - x-publishable-api-key: {your_publishable_api_key} -``` - -The Medusa application infers the associated sales channels and ensures that only data relevant to the sales channel are used. - -*** - -## How to Create a Publishable API Key? - -To create a publishable API key, either use the [Medusa Admin](https://docs.medusajs.com/user-guide/settings/developer/publishable-api-keys/index.html.md) or the [Admin API Routes](https://docs.medusajs.com/api/admin#publishable-api-keys). - - -# Links between Sales Channel Module and Other Modules - -This document showcases the module links defined between the Sales Channel Module and other Commerce Modules. - -## Summary - -The Sales Channel Module has the following links to other modules: - -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. - -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -| in ||Stored|| -| in ||Read-only|| -| in ||Read-only|| -| in ||Stored|| -|| in |Stored|| - -*** - -## API Key Module - -A publishable API key allows you to easily specify the sales channel scope in a client request. - -Medusa defines a link between the `ApiKey` and the `SalesChannel` data models. - -![A diagram showcasing an example of how resources from the Sales Channel and API Key modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1709812064/Medusa%20Resources/sales-channel-api-key_zmqi2l.jpg) - -### Retrieve with Query - -To retrieve the API keys associated with a sales channel with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `publishable_api_keys.*` in `fields`: - -### query.graph - -```ts -const { data: salesChannels } = await query.graph({ - entity: "sales_channel", - fields: [ - "publishable_api_keys.*", - ], -}) - -// salesChannels.publishable_api_keys -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: salesChannels } = useQueryGraphStep({ - entity: "sales_channel", - fields: [ - "publishable_api_keys.*", - ], -}) - -// salesChannels.publishable_api_keys -``` - -### Manage with Link - -To manage the sales channels of an API key, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.API_KEY]: { - api_key_id: "apk_123", - }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.API_KEY]: { - api_key_id: "apk_123", - }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, -}) -``` - -*** - -## 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 `SalesChannel` data model. Because the link is read-only from the `Cart`'s side, you can only retrieve the sales channel of a cart, and not the other way around. - -### Retrieve with Query - -To retrieve the sales channel of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channel.*` in `fields`: - -### query.graph - -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "sales_channel.*", - ], -}) - -// carts.sales_channel -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "sales_channel.*", - ], -}) - -// carts.sales_channel -``` - -*** - -## 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 `SalesChannel` data model. Because the link is read-only from the `Order`'s side, you can only retrieve the sales channel of an order, and not the other way around. - -### Retrieve with Query - -To retrieve the sales channel of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channel.*` in `fields`: - -### query.graph - -```ts -const { data: orders } = await query.graph({ - entity: "order", - fields: [ - "sales_channel.*", - ], -}) - -// orders.sales_channel -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: orders } = useQueryGraphStep({ - entity: "order", - fields: [ - "sales_channel.*", - ], -}) - -// orders.sales_channel -``` - -*** - -## Product Module - -A product has different availability for different sales channels. Medusa defines a link between the `Product` and the `SalesChannel` data models. - -![A diagram showcasing an example of how resources from the Sales Channel and Product modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1709809833/Medusa%20Resources/product-sales-channel_t848ik.jpg) - -A product can be available in more than one sales channel. You can retrieve only the products of a sales channel. - -### Retrieve with Query - -To retrieve the products of a sales channel with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `products.*` in `fields`: - -### query.graph - -```ts -const { data: salesChannels } = await query.graph({ - entity: "sales_channel", - fields: [ - "products.*", - ], -}) - -// salesChannels.products -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: salesChannels } = useQueryGraphStep({ - entity: "sales_channel", - fields: [ - "products.*", - ], -}) - -// salesChannels.products -``` - -### Manage with Link - -To manage the sales channels of a product, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, -}) -``` - -*** - -## Stock Location Module - -A stock location is associated with a sales channel. This scopes inventory quantities associated with that stock location by the associated sales channel. - -Medusa defines a link between the `SalesChannel` and `StockLocation` data models. - -![A diagram showcasing an example of how resources from the Sales Channel and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716796872/Medusa%20Resources/sales-channel-location_cqrih1.jpg) - -### Retrieve with Query - -To retrieve the stock locations of a sales channel with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `stock_locations.*` in `fields`: - -### query.graph - -```ts -const { data: salesChannels } = await query.graph({ - entity: "sales_channel", - fields: [ - "stock_locations.*", - ], -}) - -// salesChannels.stock_locations -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: salesChannels } = useQueryGraphStep({ - entity: "sales_channel", - fields: [ - "stock_locations.*", - ], -}) - -// salesChannels.stock_locations -``` - -### Manage with Link - -To manage the stock locations of a sales channel, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, - [Modules.STOCK_LOCATION]: { - sales_channel_id: "sloc_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, - [Modules.STOCK_LOCATION]: { - sales_channel_id: "sloc_123", - }, -}) -``` - - -# 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 Product Module and Other Modules This document showcases the module links defined between the Product Module and other Commerce Modules. @@ -28165,6 +26651,1648 @@ The following guides provide more details on inventory management in the Medusa - [Storefront guide: how to retrieve a product variant's inventory details](https://docs.medusajs.com/resources/storefront-development/products/inventory/index.html.md). +# 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.||| + + +# Pricing Concepts + +In this document, you’ll learn about the main concepts in the Pricing Module. + +## Price Set + +A [PriceSet](https://docs.medusajs.com/references/pricing/models/PriceSet/index.html.md) represents a collection of prices that are linked to a resource (for example, a product or a shipping option). + +Each of these prices are represented by the [Price data module](https://docs.medusajs.com/references/pricing/models/Price/index.html.md). + +![A diagram showcasing the relation between the price set and price](https://res.cloudinary.com/dza7lstvk/image/upload/v1709648650/Medusa%20Resources/price-set-money-amount_xeees0.jpg) + +*** + +## Price List + +A [PriceList](https://docs.medusajs.com/references/pricing/models/PriceList/index.html.md) is a group of prices only enabled if their conditions and rules are satisfied. + +A price list has optional `start_date` and `end_date` properties that indicate the date range in which a price list can be applied. + +Its associated prices are represented by the `Price` data model. + + +# Links between Pricing Module and Other Modules + +This document showcases the module links defined between the Pricing Module and other Commerce Modules. + +## Summary + +The Pricing Module has the following links to other modules: + +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +| in ||Stored|| +| in ||Stored|| + +*** + +## 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.shipping_option +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: priceSets } = useQueryGraphStep({ + entity: "price_set", + fields: [ + "shipping_option.*", + ], +}) + +// priceSets.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.variant +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: priceSets } = useQueryGraphStep({ + entity: "price_set", + fields: [ + "variant.*", + ], +}) + +// priceSets.variant +``` + +### Manage with Link + +To manage the price set of a variant, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.PRODUCT]: { + variant_id: "variant_123", + }, + [Modules.PRICING]: { + price_set_id: "pset_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.PRODUCT]: { + variant_id: "variant_123", + }, + [Modules.PRICING]: { + price_set_id: "pset_123", + }, +}) +``` + + +# Prices Calculation + +In this document, you'll learn how prices are calculated when you use the [calculatePrices method](https://docs.medusajs.com/references/pricing/calculatePrices/index.html.md) of the Pricing Module's main service. + +## calculatePrices Method + +The [calculatePrices method](https://docs.medusajs.com/references/pricing/calculatePrices/index.html.md) accepts as parameters the ID of one or more price sets and a context. + +It returns a price object with the best matching price for each price set. + +### Calculation Context + +The calculation context is an optional object passed as a second parameter to the `calculatePrices` method. It accepts rules to restrict the selected prices in the price set. + +For example: + +```ts +const price = await pricingModuleService.calculatePrices( + { id: [priceSetId] }, + { + context: { + currency_code: currencyCode, + region_id: "reg_123", + }, + } +) +``` + +In this example, you retrieve the prices in a price set for the specified currency code and region ID. + +### Returned Price Object + +For each price set, the `calculatePrices` method selects two prices: + +- A calculated price: Either a price that belongs to a price list and best matches the specified context, or the same as the original price. +- An original price, which is either: + - The same price as the calculated price if the price list it belongs to is of type `override`; + - Or a price that doesn't belong to a price list and best matches the specified context. + +Both prices are returned in an object that has the following properties: + +- id: (\`string\`) The ID of the price set from which the price was selected. +- is\_calculated\_price\_price\_list: (\`boolean\`) Whether the calculated price belongs to a price list. +- calculated\_amount: (\`number\`) The amount of the calculated price, or \`null\` if there isn't a calculated price. This is the amount shown to the customer. +- is\_original\_price\_price\_list: (\`boolean\`) Whether the original price belongs to a price list. +- original\_amount: (\`number\`) The amount of the original price, or \`null\` if there isn't an original price. This amount is useful to compare with the \`calculated\_amount\`, such as to check for discounted value. +- currency\_code: (\`string\`) The currency code of the calculated price, or \`null\` if there isn't a calculated price. +- is\_calculated\_price\_tax\_inclusive: (\`boolean\`) Whether the calculated price is tax inclusive. Learn more about tax-inclusivity in \[this document]\(../tax-inclusive-pricing/page.mdx) +- is\_original\_price\_tax\_inclusive: (\`boolean\`) Whether the original price is tax inclusive. Learn more about tax-inclusivity in \[this document]\(../tax-inclusive-pricing/page.mdx) +- calculated\_price: (\`object\`) The calculated price's price details. + + - id: (\`string\`) The ID of the price. + + - price\_list\_id: (\`string\`) The ID of the associated price list. + + - price\_list\_type: (\`string\`) The price list's type. For example, \`sale\`. + + - min\_quantity: (\`number\`) The price's min quantity condition. + + - max\_quantity: (\`number\`) The price's max quantity condition. +- original\_price: (\`object\`) The original price's price details. + + - id: (\`string\`) The ID of the price. + + - price\_list\_id: (\`string\`) The ID of the associated price list. + + - price\_list\_type: (\`string\`) The price list's type. For example, \`sale\`. + + - min\_quantity: (\`number\`) The price's min quantity condition. + + - max\_quantity: (\`number\`) The price's max quantity condition. + +*** + +## Examples + +Consider the following price set: + +```ts +const priceSet = await pricingModuleService.createPriceSets({ + prices: [ + // default price + { + amount: 500, + currency_code: "EUR", + rules: {}, + }, + // prices with rules + { + amount: 400, + currency_code: "EUR", + rules: { + region_id: "reg_123", + }, + }, + { + amount: 450, + currency_code: "EUR", + rules: { + city: "krakow", + }, + }, + { + amount: 500, + currency_code: "EUR", + rules: { + city: "warsaw", + region_id: "reg_123", + }, + }, + ], +}) +``` + +### 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 + +### Price Selection with Price List + +### Code + +```ts +const priceList = pricingModuleService.createPriceLists([{ + title: "Summer Price List", + description: "Price list for summer sale", + starts_at: Date.parse("01/10/2023").toString(), + ends_at: Date.parse("31/10/2023").toString(), + rules: { + region_id: ['PL'] + }, + type: "sale", + prices: [ + { + amount: 400, + currency_code: "EUR", + price_set_id: priceSet.id, + }, + { + amount: 450, + currency_code: "EUR", + price_set_id: priceSet.id, + }, + ], +}]); + +const price = await pricingModuleService.calculatePrices( + { id: [priceSet.id] }, + { + context: { + currency_code: "EUR", + region_id: "PL", + city: "krakow" + } + } +) +``` + +### Result + + +# Price Rules + +In this Pricing Module guide, you'll learn about price rules for price sets and price lists, and how to add rules to a price. + +## Price Rule + +You can restrict prices by rules. Each rule of a price is represented by the [PriceRule data model](https://docs.medusajs.com/references/pricing/models/PriceRule/index.html.md). + +The `Price` data model has a `rules_count` property, which indicates how many rules, represented by `PriceRule`, are applied to the price. + +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 Set Rules on a Price? + +### Using Workflows + +Medusa uses the Pricing Module to store prices of different resources, such as product variants and shipping options. + +When you manage one of these resources using [Medusa's workflows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/medusa-workflows-reference/index.html.md) or using the API routes that use them, you can set rules on a price using the `rules` property of the price object. + +For example, when creating a shipping option using the [createShippingOptionsWorkflow](https://docs.medusajs.com/resources/references/medusa-workflows/createShippingOptionsWorkflow/index.html.md) to create a shipping option, you can make the shipping price free based on the cart total: + +```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, + }, + }, + }, + ], + }], + }) +``` + +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. + +### Using Pricing Module's Service + +For most use cases, it's recommended to use [workflows](#using-workflows) instead of directly using the module's service. + +When adding a price using the [addPrices](https://docs.medusajs.com/resources/references/pricing/addPrices/index.html.md) method of the Pricing Module's service, pass the `rules` property to a price object. + +For example: + +```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 set the default price of a resource (for example, a shipping option), to `$10`. You also add a conditioned price that sets the price to `0` when the cart or order's total is greater than or equal to `$100`. + +### 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. + + +# 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). + +## computeActions Method + +The Promotion Module's main service has a [computeActions method](https://docs.medusajs.com/references/promotion/computeActions/index.html.md) that returns an array of actions to perform on a cart when one or more promotions are applied. + +Actions inform you what adjustment must be made to a cart item or shipping method. Each action is an object having the `action` property indicating the type of action. + +*** + +## Action Types + +### `addItemAdjustment` Action + +The `addItemAdjustment` action indicates that an adjustment must be made to an item. For example, removing $5 off its amount. + +This action has the following format: + +```ts +export interface AddItemAdjustmentAction { + action: "addItemAdjustment" + item_id: string + amount: number + code: string + description?: string +} +``` + +This action means that a new record should be created of the `LineItemAdjustment` data model in the Cart Module, or `OrderLineItemAdjustment` data model in the Order Module. + +Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.AddItemAdjustmentAction/index.html.md) for details on the object’s properties. + +### `removeItemAdjustment` Action + +The `removeItemAdjustment` action indicates that an adjustment must be removed from a line item. For example, remove the $5 discount. + +The `computeActions` method accepts any previous item adjustments in the `items` property of the second parameter. + +This action has the following format: + +```ts +export interface RemoveItemAdjustmentAction { + action: "removeItemAdjustment" + adjustment_id: string + description?: string + code: string +} +``` + +This action means that a new record should be removed of the `LineItemAdjustment` (or `OrderLineItemAdjustment`) with the specified ID in the `adjustment_id` property. + +Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.RemoveItemAdjustmentAction/index.html.md) for details on the object’s properties. + +### `addShippingMethodAdjustment` Action + +The `addShippingMethodAdjustment` action indicates that an adjustment must be made on a shipping method. For example, make the shipping method free. + +This action has the following format: + +```ts +export interface AddShippingMethodAdjustment { + action: "addShippingMethodAdjustment" + shipping_method_id: string + amount: number + code: string + description?: string +} +``` + +This action means that a new record should be created of the `ShippingMethodAdjustment` data model in the Cart Module, or `OrderShippingMethodAdjustment` data model in the Order Module. + +Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.AddShippingMethodAdjustment/index.html.md) for details on the object’s properties. + +### `removeShippingMethodAdjustment` Action + +The `removeShippingMethodAdjustment` action indicates that an adjustment must be removed from a shipping method. For example, remove the free shipping discount. + +The `computeActions` method accepts any previous shipping method adjustments in the `shipping_methods` property of the second parameter. + +This action has the following format: + +```ts +export interface RemoveShippingMethodAdjustment { + action: "removeShippingMethodAdjustment" + adjustment_id: string + code: string +} +``` + +When the Medusa application receives this action type, it removes the `ShippingMethodAdjustment` (or `OrderShippingMethodAdjustment`) with the specified ID in the `adjustment_id` property. + +Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.RemoveShippingMethodAdjustment/index.html.md) for details on the object’s properties. + +### `campaignBudgetExceeded` Action + +When the `campaignBudgetExceeded` action is returned, the promotions within a campaign can no longer be used as the campaign budget has been exceeded. + +This action has the following format: + +```ts +export interface CampaignBudgetExceededAction { + action: "campaignBudgetExceeded" + code: string +} +``` + +Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.CampaignBudgetExceededAction/index.html.md) for details on the object’s properties. + + +# 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 + + +# Promotion Concepts + +In this document, you’ll learn about the main promotion and rule concepts in the Promotion Module. + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/promotions/index.html.md) to learn how to manage promotions using the dashboard. + +## What is a Promotion? + +A promotion, represented by the [Promotion data model](https://docs.medusajs.com/references/promotion/models/Promotion/index.html.md), is a discount that can be applied on cart items, shipping methods, or entire orders. + +A promotion has two types: + +- `standard`: A standard promotion with rules. +- `buyget`: “A buy X get Y” promotion with rules. + +|\`standard\`|\`buyget\`| +|---|---| +|A coupon code that gives customers 10% off their entire order.|Buy two shirts and get another for free.| +|A coupon code that gives customers $15 off any shirt in their order.|Buy two shirts and get 10% off the entire order.| +|A discount applied automatically for VIP customers that removes 10% off their shipping method’s amount.|Spend $100 and get free shipping.| + +The Medusa Admin UI may not provide a way to create each of these promotion examples. However, they are supported by the Promotion Module and Medusa's workflows and API routes. + +*** + +## PromotionRule + +A promotion can be restricted by a set of rules, each rule is represented by the [PromotionRule data model](https://docs.medusajs.com/references/promotion/models/PromotionRule/index.html.md). + +For example, you can create a promotion that only customers of the `VIP` customer group can use. + +![A diagram showcasing the relation between Promotion and PromotionRule](https://res.cloudinary.com/dza7lstvk/image/upload/v1709833196/Medusa%20Resources/promotion-promotion-rule_msbx0w.jpg) + +A `PromotionRule`'s `attribute` property indicates the property's name to which this rule is applied. + +For example, `customer_group_id`. Its value is stored in the `PromotionRuleValue` data model. So, a rule can have multiple values. + +When testing whether a promotion can be applied to a cart, the rule's `attribute` property and its values are tested on the cart itself. + +For example, the cart's customer must be part of the customer group(s) indicated in the promotion rule's value. + +*** + +## Flexible Rules + +The `PromotionRule`'s `operator` property adds more flexibility to the rule’s condition rather than simple equality (`eq`). + +For example, to restrict the promotion to only `VIP` and `B2B` customer groups: + +- Add a `PromotionRule` record with its `attribute` property set to `customer_group_id` and `operator` property to `in`. +- Add two `PromotionRuleValue` records associated with the rule: one with the value `VIP` and the other `B2B`. + +![A diagram showcasing the relation between PromotionRule and PromotionRuleValue when a rule has multiple values](https://res.cloudinary.com/dza7lstvk/image/upload/v1709897383/Medusa%20Resources/promotion-promotion-rule-multiple_hctpmt.jpg) + +In this case, a customer’s group must be in the `VIP` and `B2B` set of values to use the promotion. + + +# Application Method + +In this document, you'll learn what an application method is. + +## What is an Application Method? + +The [ApplicationMethod data model](https://docs.medusajs.com/references/promotion/models/ApplicationMethod/index.html.md) defines how a promotion is applied: + +|Property|Purpose| +|---|---| +|\`type\`|Does the promotion discount a fixed amount or a percentage?| +|\`target\_type\`|Is the promotion applied on a cart item, shipping method, or the entire order?| +|\`allocation\`|Is the discounted amount applied on each item or split between the applicable items?| + +## Target Promotion Rules + +When the promotion is applied to a cart item or a shipping method, you can restrict which items/shipping methods the promotion is applied to. + +The `ApplicationMethod` data model has a collection of `PromotionRule` records to restrict which items or shipping methods the promotion applies to. The `target_rules` property represents this relation. + +![A diagram showcasing the target\_rules relation between the ApplicationMethod and PromotionRule data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709898273/Medusa%20Resources/application-method-target-rules_hqaymz.jpg) + +In this example, the promotion is only applied on products in the cart having the SKU `SHIRT`. + +*** + +## Buy Promotion Rules + +When the promotion’s type is `buyget`, you must specify the “buy X” condition. For example, a cart must have two shirts before the promotion can be applied. + +The application method has a collection of `PromotionRule` items to define the “buy X” rule. The `buy_rules` property represents this relation. + +![A diagram showcasing the buy\_rules relation between the ApplicationMethod and PromotionRule data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709898453/Medusa%20Resources/application-method-buy-rules_djjuhw.jpg) + +In this example, the cart must have two products with the SKU `SHIRT` for the promotion to be applied. + + +# Campaign + +In this document, you'll learn about campaigns. + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/promotions/campaigns/index.html.md) to learn how to manage campaigns using the dashboard. + +## What is a Campaign? + +A [Campaign](https://docs.medusajs.com/references/promotion/models/Campaign/index.html.md) combines promotions under the same conditions, such as start and end dates. + +![A diagram showcasing the relation between the Campaign and Promotion data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709899225/Medusa%20Resources/campagin-promotion_hh3qsi.jpg) + +*** + +## Campaign Limits + +Each campaign has a budget represented by the [CampaignBudget data model](https://docs.medusajs.com/references/promotion/models/CampaignBudget/index.html.md). The budget limits how many times the promotion can be used. + +There are two types of budgets: + +- `spend`: An amount that, when crossed, the promotion becomes unusable. For example, if the amount limit is set to `$100`, and the total amount of usage of this promotion crosses that threshold, the promotion can no longer be applied. +- `usage`: The number of times that a promotion can be used. For example, if the usage limit is set to `10`, the promotion can be used only 10 times by customers. After that, it can no longer be applied. + +![A diagram showcasing the relation between the Campaign and CampaignBudget data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709899463/Medusa%20Resources/campagin-budget_rvqlmi.jpg) + + +# Links between Promotion Module and Other Modules + +This document showcases the module links defined between the Promotion Module and other Commerce Modules. + +## Summary + +The Promotion Module has the following links to other modules: + +Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. + +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +| in ||Stored|| +| in ||Read-only|| +| in ||Stored|| + +*** + +## Cart Module + +A promotion can be applied on line items and shipping methods of a cart. Medusa defines a link between the `Cart` and `Promotion` data models. + +![A diagram showcasing an example of how data models from the Cart and Promotion modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711538015/Medusa%20Resources/cart-promotion_kuh9vm.jpg) + +Medusa also defines a read-only link between the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `LineItemAdjustment` data model and the `Promotion` data model. Because the link is read-only from the `LineItemAdjustment`'s side, you can only retrieve the promotion applied on a line item, and not the other way around. + +### Retrieve with Query + +To retrieve the carts that a promotion is applied on with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `carts.*` in `fields`: + +To retrieve the promotion of a line item adjustment, pass `promotion.*` in `fields`. + +### query.graph + +```ts +const { data: promotions } = await query.graph({ + entity: "promotion", + fields: [ + "carts.*", + ], +}) + +// promotions.carts +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: promotions } = useQueryGraphStep({ + entity: "promotion", + fields: [ + "carts.*", + ], +}) + +// promotions.carts +``` + +### Manage with Link + +To manage the promotions of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.PROMOTION]: { + promotion_id: "promo_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.PROMOTION]: { + promotion_id: "promo_123", + }, +}) +``` + +*** + +## Order Module + +An order is associated with the promotion applied on it. Medusa defines a link between the `Order` and `Promotion` data models. + +![A diagram showcasing an example of how data models from the Order and Promotion modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716555015/Medusa%20Resources/order-promotion_dgjzzd.jpg) + +### Retrieve with Query + +To retrieve the orders a promotion is applied on with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `orders.*` in `fields`: + +### query.graph + +```ts +const { data: promotions } = await query.graph({ + entity: "promotion", + fields: [ + "orders.*", + ], +}) + +// promotions.orders +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: promotions } = useQueryGraphStep({ + entity: "promotion", + fields: [ + "orders.*", + ], +}) + +// promotions.orders +``` + +### Manage with Link + +To manage the promotion of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.ORDER]: { + order_id: "order_123", + }, + [Modules.PROMOTION]: { + promotion_id: "promo_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.ORDER]: { + order_id: "order_123", + }, + [Modules.PROMOTION]: { + promotion_id: "promo_123", + }, +}) +``` + + +# Links between 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| +|---|---|---|---| +| in ||Read-only|| +| in ||Read-only|| +|| in |Stored|| + +*** + +## 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.region +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "region.*", + ], +}) + +// carts.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.region +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "region.*", + ], +}) + +// orders.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.payment_providers +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: regions } = useQueryGraphStep({ + entity: "region", + fields: [ + "payment_providers.*", + ], +}) + +// regions.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. + +## Summary + +The Sales Channel Module has the following links to other modules: + +Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. + +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +| in ||Stored|| +| in ||Read-only|| +| in ||Read-only|| +| in ||Stored|| +|| in |Stored|| + +*** + +## API Key Module + +A publishable API key allows you to easily specify the sales channel scope in a client request. + +Medusa defines a link between the `ApiKey` and the `SalesChannel` data models. + +![A diagram showcasing an example of how resources from the Sales Channel and API Key modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1709812064/Medusa%20Resources/sales-channel-api-key_zmqi2l.jpg) + +### Retrieve with Query + +To retrieve the API keys associated with a sales channel with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `publishable_api_keys.*` in `fields`: + +### query.graph + +```ts +const { data: salesChannels } = await query.graph({ + entity: "sales_channel", + fields: [ + "publishable_api_keys.*", + ], +}) + +// salesChannels.publishable_api_keys +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: salesChannels } = useQueryGraphStep({ + entity: "sales_channel", + fields: [ + "publishable_api_keys.*", + ], +}) + +// salesChannels.publishable_api_keys +``` + +### Manage with Link + +To manage the sales channels of an API key, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.API_KEY]: { + api_key_id: "apk_123", + }, + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.API_KEY]: { + api_key_id: "apk_123", + }, + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, +}) +``` + +*** + +## 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 `SalesChannel` data model. Because the link is read-only from the `Cart`'s side, you can only retrieve the sales channel of a cart, and not the other way around. + +### Retrieve with Query + +To retrieve the sales channel of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channel.*` in `fields`: + +### query.graph + +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "sales_channel.*", + ], +}) + +// carts.sales_channel +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "sales_channel.*", + ], +}) + +// carts.sales_channel +``` + +*** + +## 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 `SalesChannel` data model. Because the link is read-only from the `Order`'s side, you can only retrieve the sales channel of an order, and not the other way around. + +### Retrieve with Query + +To retrieve the sales channel of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channel.*` in `fields`: + +### query.graph + +```ts +const { data: orders } = await query.graph({ + entity: "order", + fields: [ + "sales_channel.*", + ], +}) + +// orders.sales_channel +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "sales_channel.*", + ], +}) + +// orders.sales_channel +``` + +*** + +## Product Module + +A product has different availability for different sales channels. Medusa defines a link between the `Product` and the `SalesChannel` data models. + +![A diagram showcasing an example of how resources from the Sales Channel and Product modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1709809833/Medusa%20Resources/product-sales-channel_t848ik.jpg) + +A product can be available in more than one sales channel. You can retrieve only the products of a sales channel. + +### Retrieve with Query + +To retrieve the products of a sales channel with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `products.*` in `fields`: + +### query.graph + +```ts +const { data: salesChannels } = await query.graph({ + entity: "sales_channel", + fields: [ + "products.*", + ], +}) + +// salesChannels.products +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: salesChannels } = useQueryGraphStep({ + entity: "sales_channel", + fields: [ + "products.*", + ], +}) + +// salesChannels.products +``` + +### Manage with Link + +To manage the sales channels of a product, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, +}) +``` + +*** + +## Stock Location Module + +A stock location is associated with a sales channel. This scopes inventory quantities associated with that stock location by the associated sales channel. + +Medusa defines a link between the `SalesChannel` and `StockLocation` data models. + +![A diagram showcasing an example of how resources from the Sales Channel and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716796872/Medusa%20Resources/sales-channel-location_cqrih1.jpg) + +### Retrieve with Query + +To retrieve the stock locations of a sales channel with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `stock_locations.*` in `fields`: + +### query.graph + +```ts +const { data: salesChannels } = await query.graph({ + entity: "sales_channel", + fields: [ + "stock_locations.*", + ], +}) + +// salesChannels.stock_locations +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: salesChannels } = useQueryGraphStep({ + entity: "sales_channel", + fields: [ + "stock_locations.*", + ], +}) + +// salesChannels.stock_locations +``` + +### Manage with Link + +To manage the stock locations of a sales channel, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, + [Modules.STOCK_LOCATION]: { + sales_channel_id: "sloc_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, + [Modules.STOCK_LOCATION]: { + sales_channel_id: "sloc_123", + }, +}) +``` + + +# Publishable API Keys with Sales Channels + +In this document, you’ll learn what publishable API keys are and how to use them with sales channels. + +## Publishable API Keys with Sales Channels + +A publishable API key, provided by the API Key Module, is a client key scoped to one or more sales channels. + +When sending a request to a Store API route, you must pass a publishable API key in the header of the request: + +```bash +curl http://localhost:9000/store/products \ + x-publishable-api-key: {your_publishable_api_key} +``` + +The Medusa application infers the associated sales channels and ensures that only data relevant to the sales channel are used. + +*** + +## How to Create a Publishable API Key? + +To create a publishable API key, either use the [Medusa Admin](https://docs.medusajs.com/user-guide/settings/developer/publishable-api-keys/index.html.md) or the [Admin API Routes](https://docs.medusajs.com/api/admin#publishable-api-keys). + + # Stock Location Concepts In this document, you’ll learn about the main concepts in the Stock Location Module. @@ -28469,123 +28597,6 @@ const { data: stores } = useQueryGraphStep({ ``` -# User Module Options - -In this document, you'll learn about the options of the User Module. - -## Module Options - -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" - -// ... - -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/user", - options: { - jwt_secret: process.env.JWT_SECRET, - }, - }, - ], -}) -``` - -|Option|Description|Required| -|---|---|---|---|---| -|\`jwt\_secret\`|A string indicating the secret used to sign the invite tokens.|Yes| - -### Environment Variables - -Make sure to add the necessary environment variables for the above options in `.env`: - -```bash -JWT_SECRET=supersecret -``` - - -# User Creation Flows - -In this document, learn the different ways to create a user using the User Module. - -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, - }) -} -``` - - # Tax Module Options In this document, you'll learn about the options of the Tax Module. @@ -28744,6 +28755,138 @@ 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. +# Tax Region + +In this document, you’ll learn about tax regions and how to use them with the Region Module. + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md) to learn how to manage tax regions using the dashboard. + +## What is a Tax Region? + +A tax region, represented by the [TaxRegion data model](https://docs.medusajs.com/references/tax/models/TaxRegion/index.html.md), stores tax settings related to a region that your store serves. + +Tax regions can inherit settings and rules from a parent tax region. + +Each tax region has tax rules and a tax provider. + + +# User Module Options + +In this document, you'll learn about the options of the User Module. + +## Module Options + +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" + +// ... + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/user", + options: { + jwt_secret: process.env.JWT_SECRET, + }, + }, + ], +}) +``` + +|Option|Description|Required| +|---|---|---|---|---| +|\`jwt\_secret\`|A string indicating the secret used to sign the invite tokens.|Yes| + +### Environment Variables + +Make sure to add the necessary environment variables for the above options in `.env`: + +```bash +JWT_SECRET=supersecret +``` + + +# User Creation Flows + +In this document, learn the different ways to create a user using the User Module. + +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, + }) +} +``` + + # 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. @@ -28806,21 +28949,6 @@ const hashConfig = \{ - [How to register a customer using email and password](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/register/index.html.md) -# Tax Region - -In this document, you’ll learn about tax regions and how to use them with the Region Module. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md) to learn how to manage tax regions using the dashboard. - -## What is a Tax Region? - -A tax region, represented by the [TaxRegion data model](https://docs.medusajs.com/references/tax/models/TaxRegion/index.html.md), stores tax settings related to a region that your store serves. - -Tax regions can inherit settings and rules from a parent tax region. - -Each tax region has tax rules and a tax provider. - - # GitHub Auth Module Provider In this document, you’ll learn about the GitHub Auth Module Provider and how to install and use it in the Auth Module. @@ -28990,116 +29118,6 @@ The [Authenticate or Login API Route](https://docs.medusajs.com/Users/shahednass - [How to implement Google social login in the storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/third-party-login/index.html.md). -# Stripe Module Provider - -In this document, you’ll learn about the Stripe Module Provider and how to configure it in the Payment Module. - -Your technical team must install the Stripe Module Provider in your Medusa application first. Then, refer to [this user guide](https://docs.medusajs.com/user-guide/settings/regions#edit-region-details/index.html.md) to learn how to enable the Stripe payment provider in a region using the Medusa Admin dashboard. - -## Register the Stripe Module Provider - -### Prerequisites - -- [Stripe account](https://stripe.com/) -- [Stripe Secret API Key](https://support.stripe.com/questions/locate-api-keys-in-the-dashboard) -- [For deployed Medusa applications, a Stripe webhook secret. Refer to the end of this guide for details on the URL and events.](https://docs.stripe.com/webhooks#add-a-webhook-endpoint) - -The Stripe Module Provider is installed by default in your application. To use it, add it to the array of providers passed to the Payment Module in `medusa-config.ts`: - -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" - -// ... - -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/payment", - options: { - providers: [ - { - resolve: "@medusajs/medusa/payment-stripe", - id: "stripe", - options: { - apiKey: process.env.STRIPE_API_KEY, - }, - }, - ], - }, - }, - ], -}) -``` - -### Environment Variables - -Make sure to add the necessary environment variables for the above options in `.env`: - -```bash -STRIPE_API_KEY= -``` - -### Module Options - -|Option|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`apiKey\`|A string indicating the Stripe Secret API key.|Yes|-| -|\`webhookSecret\`|A string indicating the Stripe webhook secret. This is only useful for deployed Medusa applications.|Yes|-| -|\`capture\`|Whether to automatically capture payment after authorization.|No|\`false\`| -|\`automatic\_payment\_methods\`|A boolean value indicating whether to enable Stripe's automatic payment methods. This is useful if you integrate services like Apple pay or Google pay.|No|\`false\`| -|\`payment\_description\`|A string used as the default description of a payment if none is available in cart.context.payment\_description.|No|-| - -*** - -## Enable Stripe Providers in a Region - -Before customers can use Stripe to complete their purchases, you must enable the Stripe payment provider(s) in the region where you want to offer this payment method. - -Refer to the [user guide](https://docs.medusajs.com/user-guide/settings/regions#edit-region-details/index.html.md) to learn how to edit a region and enable the Stripe payment provider. - -*** - -## Setup Stripe Webhooks - -For production applications, you must set up webhooks in Stripe that inform Medusa of changes and updates to payments. Refer to [Stripe's documentation](https://docs.stripe.com/webhooks#add-a-webhook-endpoint) on how to setup webhooks. - -### Webhook URL - -Medusa has a `{server_url}/hooks/payment/{provider_id}` API route that you can use to register webhooks in Stripe, where: - -- `{server_url}` is the URL to your deployed Medusa application in server mode. -- `{provider_id}` is the ID of the provider, such as `stripe_stripe` for basic payments. - -The Stripe Module Provider supports the following payment types, and the webhook endpoint URL is different for each: - -|Stripe Payment Type|Webhook Endpoint URL| -|---|---|---| -|Basic Stripe Payment|\`\{server\_url}/hooks/payment/stripe\_stripe\`| -|Bancontact Payments|\`\{server\_url}/hooks/payment/stripe-bancontact\_stripe\`| -|BLIK Payments|\`\{server\_url}/hooks/payment/stripe-blik\_stripe\`| -|giropay Payments|\`\{server\_url}/hooks/payment/stripe-giropay\_stripe\`| -|iDEAL Payments|\`\{server\_url}/hooks/payment/stripe-ideal\_stripe\`| -|Przelewy24 Payments|\`\{server\_url}/hooks/payment/stripe-przelewy24\_stripe\`| -|PromptPay Payments|\`\{server\_url}/hooks/payment/stripe-promptpay\_stripe\`| - -### Webhook Events - -When you set up the webhook in Stripe, choose the following events to listen to: - -- `payment_intent.amount_capturable_updated` -- `payment_intent.succeeded` -- `payment_intent.payment_failed` - -*** - -## Useful Guides - -- [Storefront guide: Add Stripe payment method during checkout](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/checkout/payment/stripe/index.html.md). -- [Integrate in Next.js Starter](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter#stripe-integration/index.html.md). -- [Customize Stripe Integration in Next.js Starter](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/guides/customize-stripe/index.html.md). - - # Get Product Variant Prices using Query In this document, you'll learn how to retrieve product variant prices in the Medusa application using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md). @@ -29365,318 +29383,428 @@ For each product variant, you: - `priceWithoutTax`: The variant's price without taxes applied. +# Stripe Module Provider + +In this document, you’ll learn about the Stripe Module Provider and how to configure it in the Payment Module. + +Your technical team must install the Stripe Module Provider in your Medusa application first. Then, refer to [this user guide](https://docs.medusajs.com/user-guide/settings/regions#edit-region-details/index.html.md) to learn how to enable the Stripe payment provider in a region using the Medusa Admin dashboard. + +## Register the Stripe Module Provider + +### Prerequisites + +- [Stripe account](https://stripe.com/) +- [Stripe Secret API Key](https://support.stripe.com/questions/locate-api-keys-in-the-dashboard) +- [For deployed Medusa applications, a Stripe webhook secret. Refer to the end of this guide for details on the URL and events.](https://docs.stripe.com/webhooks#add-a-webhook-endpoint) + +The Stripe Module Provider is installed by default in your application. To use it, add it to the array of providers passed to the Payment Module in `medusa-config.ts`: + +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" + +// ... + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/payment", + options: { + providers: [ + { + resolve: "@medusajs/medusa/payment-stripe", + id: "stripe", + options: { + apiKey: process.env.STRIPE_API_KEY, + }, + }, + ], + }, + }, + ], +}) +``` + +### Environment Variables + +Make sure to add the necessary environment variables for the above options in `.env`: + +```bash +STRIPE_API_KEY= +``` + +### Module Options + +|Option|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`apiKey\`|A string indicating the Stripe Secret API key.|Yes|-| +|\`webhookSecret\`|A string indicating the Stripe webhook secret. This is only useful for deployed Medusa applications.|Yes|-| +|\`capture\`|Whether to automatically capture payment after authorization.|No|\`false\`| +|\`automatic\_payment\_methods\`|A boolean value indicating whether to enable Stripe's automatic payment methods. This is useful if you integrate services like Apple pay or Google pay.|No|\`false\`| +|\`payment\_description\`|A string used as the default description of a payment if none is available in cart.context.payment\_description.|No|-| + +*** + +## Enable Stripe Providers in a Region + +Before customers can use Stripe to complete their purchases, you must enable the Stripe payment provider(s) in the region where you want to offer this payment method. + +Refer to the [user guide](https://docs.medusajs.com/user-guide/settings/regions#edit-region-details/index.html.md) to learn how to edit a region and enable the Stripe payment provider. + +*** + +## Setup Stripe Webhooks + +For production applications, you must set up webhooks in Stripe that inform Medusa of changes and updates to payments. Refer to [Stripe's documentation](https://docs.stripe.com/webhooks#add-a-webhook-endpoint) on how to setup webhooks. + +### Webhook URL + +Medusa has a `{server_url}/hooks/payment/{provider_id}` API route that you can use to register webhooks in Stripe, where: + +- `{server_url}` is the URL to your deployed Medusa application in server mode. +- `{provider_id}` is the ID of the provider, such as `stripe_stripe` for basic payments. + +The Stripe Module Provider supports the following payment types, and the webhook endpoint URL is different for each: + +|Stripe Payment Type|Webhook Endpoint URL| +|---|---|---| +|Basic Stripe Payment|\`\{server\_url}/hooks/payment/stripe\_stripe\`| +|Bancontact Payments|\`\{server\_url}/hooks/payment/stripe-bancontact\_stripe\`| +|BLIK Payments|\`\{server\_url}/hooks/payment/stripe-blik\_stripe\`| +|giropay Payments|\`\{server\_url}/hooks/payment/stripe-giropay\_stripe\`| +|iDEAL Payments|\`\{server\_url}/hooks/payment/stripe-ideal\_stripe\`| +|Przelewy24 Payments|\`\{server\_url}/hooks/payment/stripe-przelewy24\_stripe\`| +|PromptPay Payments|\`\{server\_url}/hooks/payment/stripe-promptpay\_stripe\`| + +### Webhook Events + +When you set up the webhook in Stripe, choose the following events to listen to: + +- `payment_intent.amount_capturable_updated` +- `payment_intent.succeeded` +- `payment_intent.payment_failed` + +*** + +## Useful Guides + +- [Storefront guide: Add Stripe payment method during checkout](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/checkout/payment/stripe/index.html.md). +- [Integrate in Next.js Starter](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter#stripe-integration/index.html.md). +- [Customize Stripe Integration in Next.js Starter](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/guides/customize-stripe/index.html.md). + + ## 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) - [linkSalesChannelsToApiKeyWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkSalesChannelsToApiKeyWorkflow/index.html.md) -- [revokeApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/revokeApiKeysWorkflow/index.html.md) - [updateApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateApiKeysWorkflow/index.html.md) -- [createCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAccountWorkflow/index.html.md) -- [deleteCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomerAddressesWorkflow/index.html.md) -- [deleteCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomersWorkflow/index.html.md) -- [updateCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerAddressesWorkflow/index.html.md) -- [removeCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeCustomerAccountWorkflow/index.html.md) -- [updateCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomersWorkflow/index.html.md) -- [createCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomersWorkflow/index.html.md) -- [createCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAddressesWorkflow/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) -- [batchLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinksWorkflow/index.html.md) -- [createLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/createLinksWorkflow/index.html.md) +- [revokeApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/revokeApiKeysWorkflow/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) -- [generateResetPasswordTokenWorkflow](https://docs.medusajs.com/references/medusa-workflows/generateResetPasswordTokenWorkflow/index.html.md) - [createCartCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartCreditLinesWorkflow/index.html.md) - [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) - [confirmVariantInventoryWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmVariantInventoryWorkflow/index.html.md) -- [deleteCartCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCartCreditLinesWorkflow/index.html.md) -- [listShippingOptionsForCartWithPricingWorkflow](https://docs.medusajs.com/references/medusa-workflows/listShippingOptionsForCartWithPricingWorkflow/index.html.md) -- [listShippingOptionsForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/listShippingOptionsForCartWorkflow/index.html.md) -- [refreshCartItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartItemsWorkflow/index.html.md) - [createCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartWorkflow/index.html.md) - [createPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentCollectionForCartWorkflow/index.html.md) -- [refreshCartShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartShippingMethodsWorkflow/index.html.md) +- [listShippingOptionsForCartWithPricingWorkflow](https://docs.medusajs.com/references/medusa-workflows/listShippingOptionsForCartWithPricingWorkflow/index.html.md) +- [deleteCartCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCartCreditLinesWorkflow/index.html.md) +- [listShippingOptionsForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/listShippingOptionsForCartWorkflow/index.html.md) +- [refreshCartItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartItemsWorkflow/index.html.md) - [refreshPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshPaymentCollectionForCartWorkflow/index.html.md) +- [refreshCartShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartShippingMethodsWorkflow/index.html.md) +- [transferCartCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/transferCartCustomerWorkflow/index.html.md) - [updateCartPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartPromotionsWorkflow/index.html.md) +- [updateCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartWorkflow/index.html.md) - [updateTaxLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxLinesWorkflow/index.html.md) - [updateLineItemInCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateLineItemInCartWorkflow/index.html.md) -- [updateCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartWorkflow/index.html.md) -- [transferCartCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/transferCartCustomerWorkflow/index.html.md) - [validateExistingPaymentCollectionStep](https://docs.medusajs.com/references/medusa-workflows/validateExistingPaymentCollectionStep/index.html.md) +- [linkCustomerGroupsToCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkCustomerGroupsToCustomerWorkflow/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) - [updateCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerGroupsWorkflow/index.html.md) - [linkCustomersToCustomerGroupWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkCustomersToCustomerGroupWorkflow/index.html.md) -- [linkCustomerGroupsToCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkCustomerGroupsToCustomerWorkflow/index.html.md) -- [deleteCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomerGroupsWorkflow/index.html.md) +- [createDefaultsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createDefaultsWorkflow/index.html.md) - [uploadFilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/uploadFilesWorkflow/index.html.md) - [deleteFilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteFilesWorkflow/index.html.md) -- [createDefaultsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createDefaultsWorkflow/index.html.md) -- [batchInventoryItemLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchInventoryItemLevelsWorkflow/index.html.md) -- [createInventoryItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInventoryItemsWorkflow/index.html.md) -- [bulkCreateDeleteLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/bulkCreateDeleteLevelsWorkflow/index.html.md) -- [createInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInventoryLevelsWorkflow/index.html.md) -- [deleteInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInventoryLevelsWorkflow/index.html.md) -- [updateInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateInventoryLevelsWorkflow/index.html.md) -- [updateInventoryItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateInventoryItemsWorkflow/index.html.md) -- [deleteInventoryItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInventoryItemWorkflow/index.html.md) -- [validateInventoryLevelsDelete](https://docs.medusajs.com/references/medusa-workflows/validateInventoryLevelsDelete/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) - [calculateShippingOptionsPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/calculateShippingOptionsPricesWorkflow/index.html.md) -- [cancelFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelFulfillmentWorkflow/index.html.md) - [createFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createFulfillmentWorkflow/index.html.md) -- [createServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createServiceZonesWorkflow/index.html.md) - [createReturnFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnFulfillmentWorkflow/index.html.md) +- [createServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createServiceZonesWorkflow/index.html.md) - [createShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShipmentWorkflow/index.html.md) - [createShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShippingOptionsWorkflow/index.html.md) -- [deleteFulfillmentSetsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteFulfillmentSetsWorkflow/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) - [updateFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateFulfillmentWorkflow/index.html.md) - [updateServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateServiceZonesWorkflow/index.html.md) -- [validateFulfillmentDeliverabilityStep](https://docs.medusajs.com/references/medusa-workflows/validateFulfillmentDeliverabilityStep/index.html.md) -- [updateShippingProfilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateShippingProfilesWorkflow/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) +- [createInventoryItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInventoryItemsWorkflow/index.html.md) +- [bulkCreateDeleteLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/bulkCreateDeleteLevelsWorkflow/index.html.md) +- [createInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInventoryLevelsWorkflow/index.html.md) +- [batchInventoryItemLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchInventoryItemLevelsWorkflow/index.html.md) +- [deleteInventoryItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInventoryItemWorkflow/index.html.md) +- [deleteInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInventoryLevelsWorkflow/index.html.md) +- [updateInventoryItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateInventoryItemsWorkflow/index.html.md) +- [updateInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateInventoryLevelsWorkflow/index.html.md) +- [validateInventoryLevelsDelete](https://docs.medusajs.com/references/medusa-workflows/validateInventoryLevelsDelete/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) +- [updateCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomersWorkflow/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) - [acceptInviteWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptInviteWorkflow/index.html.md) -- [createInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInvitesWorkflow/index.html.md) -- [refreshInviteTokensWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshInviteTokensWorkflow/index.html.md) - [deleteInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInvitesWorkflow/index.html.md) +- [refreshInviteTokensWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshInviteTokensWorkflow/index.html.md) +- [createInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInvitesWorkflow/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) +- [deleteLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteLineItemsWorkflow/index.html.md) +- [updateLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateLinksWorkflow/index.html.md) +- [dismissLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/dismissLinksWorkflow/index.html.md) - [capturePaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/capturePaymentWorkflow/index.html.md) - [processPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/processPaymentWorkflow/index.html.md) - [refundPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentWorkflow/index.html.md) -- [validatePaymentsRefundStep](https://docs.medusajs.com/references/medusa-workflows/validatePaymentsRefundStep/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) -- [deleteLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteLineItemsWorkflow/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) - [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) - [updatePriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePriceListPricesWorkflow/index.html.md) -- [createPriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListsWorkflow/index.html.md) - [updatePriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePriceListsWorkflow/index.html.md) +- [deletePaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePaymentSessionsWorkflow/index.html.md) - [createPaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentSessionsWorkflow/index.html.md) - [createRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRefundReasonsWorkflow/index.html.md) -- [deletePaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePaymentSessionsWorkflow/index.html.md) - [updateRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRefundReasonsWorkflow/index.html.md) - [deleteRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRefundReasonsWorkflow/index.html.md) - [acceptOrderTransferValidationStep](https://docs.medusajs.com/references/medusa-workflows/acceptOrderTransferValidationStep/index.html.md) - [acceptOrderTransferWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptOrderTransferWorkflow/index.html.md) -- [archiveOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/archiveOrderWorkflow/index.html.md) -- [addOrderLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addOrderLineItemsWorkflow/index.html.md) - [beginClaimOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginClaimOrderValidationStep/index.html.md) +- [addOrderLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addOrderLineItemsWorkflow/index.html.md) +- [archiveOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/archiveOrderWorkflow/index.html.md) - [beginClaimOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginClaimOrderWorkflow/index.html.md) - [beginExchangeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginExchangeOrderWorkflow/index.html.md) -- [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) -- [beginReceiveReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginReceiveReturnWorkflow/index.html.md) -- [beginReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginReturnOrderWorkflow/index.html.md) -- [beginReturnOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginReturnOrderValidationStep/index.html.md) +- [beginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginOrderExchangeValidationStep/index.html.md) +- [beginOrderEditOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditOrderWorkflow/index.html.md) - [beginReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginReceiveReturnValidationStep/index.html.md) +- [beginReceiveReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginReceiveReturnWorkflow/index.html.md) +- [beginReturnOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginReturnOrderValidationStep/index.html.md) - [cancelBeginOrderClaimValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderClaimValidationStep/index.html.md) -- [cancelBeginOrderEditValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditValidationStep/index.html.md) - [cancelBeginOrderClaimWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderClaimWorkflow/index.html.md) -- [cancelBeginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeValidationStep/index.html.md) -- [cancelClaimValidateOrderStep](https://docs.medusajs.com/references/medusa-workflows/cancelClaimValidateOrderStep/index.html.md) +- [cancelBeginOrderEditValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditValidationStep/index.html.md) +- [beginReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginReturnOrderWorkflow/index.html.md) - [cancelBeginOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeWorkflow/index.html.md) +- [cancelBeginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeValidationStep/index.html.md) +- [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) - [cancelExchangeValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelExchangeValidateOrder/index.html.md) - [cancelOrderClaimWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderClaimWorkflow/index.html.md) -- [cancelOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderChangeWorkflow/index.html.md) - [cancelOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderExchangeWorkflow/index.html.md) -- [cancelOrderFulfillmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentValidateOrder/index.html.md) -- [cancelBeginOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditWorkflow/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) +- [cancelOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentWorkflow/index.html.md) +- [cancelOrderFulfillmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentValidateOrder/index.html.md) +- [cancelOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderWorkflow/index.html.md) - [cancelReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelReceiveReturnValidationStep/index.html.md) - [cancelReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnReceiveWorkflow/index.html.md) -- [cancelOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderWorkflow/index.html.md) +- [cancelRequestReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelRequestReturnValidationStep/index.html.md) - [cancelReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnRequestWorkflow/index.html.md) - [cancelReturnValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelReturnValidateOrder/index.html.md) +- [cancelReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnWorkflow/index.html.md) - [cancelTransferOrderRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelTransferOrderRequestValidationStep/index.html.md) - [cancelValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelValidateOrder/index.html.md) -- [cancelReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnWorkflow/index.html.md) -- [cancelRequestReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelRequestReturnValidationStep/index.html.md) +- [completeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeOrderWorkflow/index.html.md) - [confirmClaimRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmClaimRequestValidationStep/index.html.md) - [confirmClaimRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmClaimRequestWorkflow/index.html.md) - [confirmExchangeRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestValidationStep/index.html.md) -- [confirmOrderEditRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestValidationStep/index.html.md) - [confirmOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestWorkflow/index.html.md) -- [completeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeOrderWorkflow/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) - [confirmReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmReceiveReturnValidationStep/index.html.md) - [confirmReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmReturnRequestValidationStep/index.html.md) -- [confirmExchangeRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestWorkflow/index.html.md) +- [confirmReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnRequestWorkflow/index.html.md) - [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/index.html.md) - [createAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createAndCompleteReturnOrderWorkflow/index.html.md) - [createClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createClaimShippingMethodValidationStep/index.html.md) -- [confirmReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnRequestWorkflow/index.html.md) +- [createCompleteReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/createCompleteReturnValidationStep/index.html.md) - [createClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createClaimShippingMethodWorkflow/index.html.md) - [createExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createExchangeShippingMethodValidationStep/index.html.md) -- [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) - [createOrUpdateOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrUpdateOrderPaymentCollectionWorkflow/index.html.md) +- [createFulfillmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/createFulfillmentValidateOrder/index.html.md) +- [createExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createExchangeShippingMethodWorkflow/index.html.md) - [createOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderChangeActionsWorkflow/index.html.md) - [createOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createOrderEditShippingMethodValidationStep/index.html.md) - [createOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderChangeWorkflow/index.html.md) - [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md) - [createOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderEditShippingMethodWorkflow/index.html.md) -- [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) +- [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) - [createReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnShippingMethodWorkflow/index.html.md) +- [createOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderWorkflow/index.html.md) +- [createReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createReturnShippingMethodValidationStep/index.html.md) - [createShipmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/createShipmentValidateOrder/index.html.md) -- [declineOrderTransferRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/declineOrderTransferRequestWorkflow/index.html.md) - [declineOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/declineOrderChangeWorkflow/index.html.md) -- [declineTransferOrderRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/declineTransferOrderRequestValidationStep/index.html.md) -- [deleteOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteOrderChangeActionsWorkflow/index.html.md) +- [declineOrderTransferRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/declineOrderTransferRequestWorkflow/index.html.md) - [deleteOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteOrderChangeWorkflow/index.html.md) - [deleteOrderPaymentCollections](https://docs.medusajs.com/references/medusa-workflows/deleteOrderPaymentCollections/index.html.md) +- [declineTransferOrderRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/declineTransferOrderRequestValidationStep/index.html.md) +- [deleteOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteOrderChangeActionsWorkflow/index.html.md) - [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) +- [getOrdersListWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrdersListWorkflow/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) -- [getOrdersListWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrdersListWorkflow/index.html.md) -- [markPaymentCollectionAsPaid](https://docs.medusajs.com/references/medusa-workflows/markPaymentCollectionAsPaid/index.html.md) - [orderClaimAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimAddNewItemValidationStep/index.html.md) +- [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) - [orderClaimAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimAddNewItemWorkflow/index.html.md) -- [orderClaimRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimRequestItemReturnValidationStep/index.html.md) - [orderClaimItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimItemWorkflow/index.html.md) +- [orderClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimItemValidationStep/index.html.md) +- [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) -- [orderEditUpdateItemQuantityWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderEditUpdateItemQuantityWorkflow/index.html.md) +- [orderEditAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderEditAddNewItemWorkflow/index.html.md) - [orderExchangeAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeAddNewItemWorkflow/index.html.md) +- [orderEditUpdateItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderEditUpdateItemQuantityValidationStep/index.html.md) - [orderExchangeRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeRequestItemReturnWorkflow/index.html.md) -- [receiveAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveAndCompleteReturnOrderWorkflow/index.html.md) +- [orderEditUpdateItemQuantityWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderEditUpdateItemQuantityWorkflow/index.html.md) - [orderFulfillmentDeliverablilityValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderFulfillmentDeliverablilityValidationStep/index.html.md) +- [receiveAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveAndCompleteReturnOrderWorkflow/index.html.md) +- [receiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestWorkflow/index.html.md) - [receiveCompleteReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/receiveCompleteReturnValidationStep/index.html.md) - [receiveItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestValidationStep/index.html.md) -- [removeAddItemClaimActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeAddItemClaimActionWorkflow/index.html.md) -- [receiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestWorkflow/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) - [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) - [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) - [removeClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeClaimShippingMethodWorkflow/index.html.md) +- [removeExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeExchangeShippingMethodWorkflow/index.html.md) - [removeItemClaimActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemClaimActionWorkflow/index.html.md) -- [removeItemExchangeActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemExchangeActionWorkflow/index.html.md) -- [removeItemOrderEditActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemOrderEditActionWorkflow/index.html.md) - [removeItemReceiveReturnActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeItemReceiveReturnActionValidationStep/index.html.md) -- [removeOrderEditItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditItemActionValidationStep/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) - [removeItemReturnActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemReturnActionWorkflow/index.html.md) +- [removeOrderEditItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditItemActionValidationStep/index.html.md) - [removeItemReceiveReturnActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemReceiveReturnActionWorkflow/index.html.md) - [removeOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditShippingMethodValidationStep/index.html.md) -- [removeOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditShippingMethodWorkflow/index.html.md) - [removeReturnItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeReturnItemActionValidationStep/index.html.md) +- [removeOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditShippingMethodWorkflow/index.html.md) - [removeReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeReturnShippingMethodValidationStep/index.html.md) - [removeReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeReturnShippingMethodWorkflow/index.html.md) - [requestOrderEditRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestOrderEditRequestValidationStep/index.html.md) - [requestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestItemReturnWorkflow/index.html.md) -- [requestOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestOrderEditRequestWorkflow/index.html.md) - [requestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestItemReturnValidationStep/index.html.md) +- [requestOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestOrderEditRequestWorkflow/index.html.md) - [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) -- [updateClaimAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimAddItemWorkflow/index.html.md) - [throwUnlessPaymentCollectionNotPaid](https://docs.medusajs.com/references/medusa-workflows/throwUnlessPaymentCollectionNotPaid/index.html.md) -- [updateClaimItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemWorkflow/index.html.md) -- [updateClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemValidationStep/index.html.md) +- [throwUnlessStatusIsNotPaid](https://docs.medusajs.com/references/medusa-workflows/throwUnlessStatusIsNotPaid/index.html.md) - [updateClaimAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimAddItemValidationStep/index.html.md) +- [updateClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemValidationStep/index.html.md) +- [updateClaimAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimAddItemWorkflow/index.html.md) - [updateClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimShippingMethodValidationStep/index.html.md) - [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) +- [updateClaimItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemWorkflow/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) +- [updateExchangeAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateExchangeAddItemWorkflow/index.html.md) - [updateExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateExchangeShippingMethodValidationStep/index.html.md) +- [updateOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderChangeActionsWorkflow/index.html.md) - [updateOrderEditAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditAddItemWorkflow/index.html.md) - [updateOrderEditAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditAddItemValidationStep/index.html.md) -- [updateOrderEditItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityValidationStep/index.html.md) +- [updateOrderChangesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderChangesWorkflow/index.html.md) - [updateOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditShippingMethodValidationStep/index.html.md) -- [updateOrderTaxLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderTaxLinesWorkflow/index.html.md) - [updateOrderEditItemQuantityWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityWorkflow/index.html.md) -- [updateOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderWorkflow/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) - [updateOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditShippingMethodWorkflow/index.html.md) - [updateOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderValidationStep/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) +- [updateOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderWorkflow/index.html.md) - [updateReceiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReceiveItemReturnRequestWorkflow/index.html.md) -- [updateReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReturnValidationStep/index.html.md) -- [updateReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnShippingMethodWorkflow/index.html.md) +- [updateRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnValidationStep/index.html.md) +- [updateReceiveItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReceiveItemReturnRequestValidationStep/index.html.md) +- [updateRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnWorkflow/index.html.md) - [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) - [createPricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPricePreferencesWorkflow/index.html.md) - [deletePricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePricePreferencesWorkflow/index.html.md) - [updatePricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePricePreferencesWorkflow/index.html.md) +- [deleteRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRegionsWorkflow/index.html.md) +- [createRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRegionsWorkflow/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) +- [updateRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRegionsWorkflow/index.html.md) +- [updateProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductCategoriesWorkflow/index.html.md) - [batchProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductVariantsWorkflow/index.html.md) - [batchLinkProductsToCategoryWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinkProductsToCategoryWorkflow/index.html.md) - [batchLinkProductsToCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinkProductsToCollectionWorkflow/index.html.md) +- [createProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductOptionsWorkflow/index.html.md) - [batchProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductsWorkflow/index.html.md) - [createCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCollectionsWorkflow/index.html.md) -- [createProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTypesWorkflow/index.html.md) -- [createProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductOptionsWorkflow/index.html.md) -- [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/index.html.md) - [createProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTagsWorkflow/index.html.md) - [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/index.html.md) -- [deleteCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCollectionsWorkflow/index.html.md) +- [createProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTypesWorkflow/index.html.md) +- [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/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) +- [deleteCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCollectionsWorkflow/index.html.md) - [deleteProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductsWorkflow/index.html.md) -- [importProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsWorkflow/index.html.md) +- [deleteProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTypesWorkflow/index.html.md) - [deleteProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductVariantsWorkflow/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) +- [exportProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/exportProductsWorkflow/index.html.md) +- [updateCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCollectionsWorkflow/index.html.md) +- [importProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsWorkflow/index.html.md) - [updateProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTagsWorkflow/index.html.md) - [updateProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTypesWorkflow/index.html.md) - [updateProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductsWorkflow/index.html.md) - [upsertVariantPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/upsertVariantPricesWorkflow/index.html.md) - [updateProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductVariantsWorkflow/index.html.md) - [validateProductInputStep](https://docs.medusajs.com/references/medusa-workflows/validateProductInputStep/index.html.md) -- [createProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductCategoriesWorkflow/index.html.md) -- [deleteProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductCategoriesWorkflow/index.html.md) -- [updateProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductCategoriesWorkflow/index.html.md) - [batchPromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchPromotionRulesWorkflow/index.html.md) - [addOrRemoveCampaignPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addOrRemoveCampaignPromotionsWorkflow/index.html.md) -- [createPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionsWorkflow/index.html.md) - [createCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCampaignsWorkflow/index.html.md) - [deleteCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCampaignsWorkflow/index.html.md) - [createPromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionRulesWorkflow/index.html.md) +- [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) - [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) - [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) +- [deleteReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReturnReasonsWorkflow/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) +- [createSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createSalesChannelsWorkflow/index.html.md) +- [deleteSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteSalesChannelsWorkflow/index.html.md) +- [updateSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateSalesChannelsWorkflow/index.html.md) +- [linkProductsToSalesChannelWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkProductsToSalesChannelWorkflow/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) - [deleteReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReservationsWorkflow/index.html.md) - [updateReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReservationsWorkflow/index.html.md) -- [deleteReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReturnReasonsWorkflow/index.html.md) -- [createReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnReasonsWorkflow/index.html.md) -- [updateReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnReasonsWorkflow/index.html.md) -- [deleteSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteSalesChannelsWorkflow/index.html.md) -- [linkProductsToSalesChannelWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkProductsToSalesChannelWorkflow/index.html.md) -- [createSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createSalesChannelsWorkflow/index.html.md) -- [updateSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateSalesChannelsWorkflow/index.html.md) - [createLocationFulfillmentSetWorkflow](https://docs.medusajs.com/references/medusa-workflows/createLocationFulfillmentSetWorkflow/index.html.md) - [deleteStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteStockLocationsWorkflow/index.html.md) - [createStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createStockLocationsWorkflow/index.html.md) @@ -29684,35 +29812,69 @@ For each product variant, you: - [updateStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateStockLocationsWorkflow/index.html.md) - [deleteShippingProfileWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteShippingProfileWorkflow/index.html.md) - [validateStepShippingProfileDelete](https://docs.medusajs.com/references/medusa-workflows/validateStepShippingProfileDelete/index.html.md) -- [createTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRateRulesWorkflow/index.html.md) -- [deleteTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRateRulesWorkflow/index.html.md) -- [createTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRegionsWorkflow/index.html.md) -- [createTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRatesWorkflow/index.html.md) -- [deleteTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRegionsWorkflow/index.html.md) -- [deleteTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRatesWorkflow/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) -- [maybeListTaxRateRuleIdsStep](https://docs.medusajs.com/references/medusa-workflows/maybeListTaxRateRuleIdsStep/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) - [createStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/createStoresWorkflow/index.html.md) - [deleteStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteStoresWorkflow/index.html.md) +- [updateStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateStoresWorkflow/index.html.md) +- [createUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUserAccountWorkflow/index.html.md) - [createUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUsersWorkflow/index.html.md) - [deleteUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteUsersWorkflow/index.html.md) - [removeUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeUserAccountWorkflow/index.html.md) -- [createUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUserAccountWorkflow/index.html.md) - [updateUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateUsersWorkflow/index.html.md) +- [createTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRateRulesWorkflow/index.html.md) +- [createTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRatesWorkflow/index.html.md) +- [deleteTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRateRulesWorkflow/index.html.md) +- [createTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRegionsWorkflow/index.html.md) +- [deleteTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRegionsWorkflow/index.html.md) +- [updateTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxRatesWorkflow/index.html.md) +- [maybeListTaxRateRuleIdsStep](https://docs.medusajs.com/references/medusa-workflows/maybeListTaxRateRuleIdsStep/index.html.md) +- [setTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/setTaxRateRulesWorkflow/index.html.md) +- [deleteTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRatesWorkflow/index.html.md) +- [updateTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxRegionsWorkflow/index.html.md) ## Steps -- [setAuthAppMetadataStep](https://docs.medusajs.com/references/medusa-workflows/steps/setAuthAppMetadataStep/index.html.md) - [createApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/createApiKeysStep/index.html.md) -- [deleteApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteApiKeysStep/index.html.md) - [linkSalesChannelsToApiKeyStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkSalesChannelsToApiKeyStep/index.html.md) -- [revokeApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/revokeApiKeysStep/index.html.md) +- [deleteApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteApiKeysStep/index.html.md) - [updateApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateApiKeysStep/index.html.md) +- [revokeApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/revokeApiKeysStep/index.html.md) - [validateSalesChannelsExistStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateSalesChannelsExistStep/index.html.md) +- [setAuthAppMetadataStep](https://docs.medusajs.com/references/medusa-workflows/steps/setAuthAppMetadataStep/index.html.md) +- [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) +- [createLineItemAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createLineItemAdjustmentsStep/index.html.md) +- [confirmInventoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/confirmInventoryStep/index.html.md) +- [createLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createLineItemsStep/index.html.md) +- [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) +- [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) +- [createShippingMethodAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingMethodAdjustmentsStep/index.html.md) +- [getActionsToComputeFromPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getActionsToComputeFromPromotionsStep/index.html.md) +- [getPromotionCodesToApply](https://docs.medusajs.com/references/medusa-workflows/steps/getPromotionCodesToApply/index.html.md) +- [getLineItemActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getLineItemActionsStep/index.html.md) +- [getVariantPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantPriceSetsStep/index.html.md) +- [prepareAdjustmentsFromPromotionActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/prepareAdjustmentsFromPromotionActionsStep/index.html.md) +- [removeLineItemAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeLineItemAdjustmentsStep/index.html.md) +- [getVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantsStep/index.html.md) +- [removeShippingMethodAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeShippingMethodAdjustmentsStep/index.html.md) +- [removeShippingMethodFromCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeShippingMethodFromCartStep/index.html.md) +- [reserveInventoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/reserveInventoryStep/index.html.md) +- [setTaxLinesForItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/setTaxLinesForItemsStep/index.html.md) +- [retrieveCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/retrieveCartStep/index.html.md) +- [updateCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartsStep/index.html.md) +- [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) +- [validateCartShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartShippingOptionsStep/index.html.md) +- [validateCartShippingOptionsPriceStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartShippingOptionsPriceStep/index.html.md) +- [validateCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartStep/index.html.md) +- [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) - [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) @@ -29721,44 +29883,10 @@ For each product variant, you: - [useQueryGraphStep](https://docs.medusajs.com/references/medusa-workflows/steps/useQueryGraphStep/index.html.md) - [useRemoteQueryStep](https://docs.medusajs.com/references/medusa-workflows/steps/useRemoteQueryStep/index.html.md) - [validatePresenceOfStep](https://docs.medusajs.com/references/medusa-workflows/steps/validatePresenceOfStep/index.html.md) -- [addShippingMethodToCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/addShippingMethodToCartStep/index.html.md) -- [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) -- [createLineItemAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createLineItemAdjustmentsStep/index.html.md) -- [createLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createLineItemsStep/index.html.md) -- [createPaymentCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentCollectionsStep/index.html.md) -- [findOneOrAnyRegionStep](https://docs.medusajs.com/references/medusa-workflows/steps/findOneOrAnyRegionStep/index.html.md) -- [findOrCreateCustomerStep](https://docs.medusajs.com/references/medusa-workflows/steps/findOrCreateCustomerStep/index.html.md) -- [createShippingMethodAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingMethodAdjustmentsStep/index.html.md) -- [findSalesChannelStep](https://docs.medusajs.com/references/medusa-workflows/steps/findSalesChannelStep/index.html.md) -- [getActionsToComputeFromPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getActionsToComputeFromPromotionsStep/index.html.md) -- [getLineItemActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getLineItemActionsStep/index.html.md) -- [getPromotionCodesToApply](https://docs.medusajs.com/references/medusa-workflows/steps/getPromotionCodesToApply/index.html.md) -- [getVariantPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantPriceSetsStep/index.html.md) -- [getVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantsStep/index.html.md) -- [prepareAdjustmentsFromPromotionActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/prepareAdjustmentsFromPromotionActionsStep/index.html.md) -- [removeLineItemAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeLineItemAdjustmentsStep/index.html.md) -- [removeShippingMethodAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeShippingMethodAdjustmentsStep/index.html.md) -- [reserveInventoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/reserveInventoryStep/index.html.md) -- [removeShippingMethodFromCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeShippingMethodFromCartStep/index.html.md) -- [retrieveCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/retrieveCartStep/index.html.md) -- [setTaxLinesForItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/setTaxLinesForItemsStep/index.html.md) -- [updateCartPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartPromotionsStep/index.html.md) -- [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) -- [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) -- [validateCartShippingOptionsPriceStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartShippingOptionsPriceStep/index.html.md) -- [validateCartShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartShippingOptionsStep/index.html.md) -- [validateCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartStep/index.html.md) -- [validateLineItemPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateLineItemPricesStep/index.html.md) -- [validateShippingStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShippingStep/index.html.md) -- [validateVariantPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateVariantPricesStep/index.html.md) - [createCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomerAddressesStep/index.html.md) - [createCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomersStep/index.html.md) -- [deleteCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomerAddressesStep/index.html.md) - [deleteCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomersStep/index.html.md) +- [deleteCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomerAddressesStep/index.html.md) - [maybeUnsetDefaultBillingAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/maybeUnsetDefaultBillingAddressesStep/index.html.md) - [maybeUnsetDefaultShippingAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/maybeUnsetDefaultShippingAddressesStep/index.html.md) - [updateCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomerAddressesStep/index.html.md) @@ -29766,198 +29894,198 @@ For each product variant, you: - [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) +- [linkCustomerGroupsToCustomerStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkCustomerGroupsToCustomerStep/index.html.md) - [linkCustomersToCustomerGroupStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkCustomersToCustomerGroupStep/index.html.md) - [updateCustomerGroupsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomerGroupsStep/index.html.md) - [createDefaultStoreStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultStoreStep/index.html.md) -- [linkCustomerGroupsToCustomerStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkCustomerGroupsToCustomerStep/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) -- [attachInventoryItemToVariants](https://docs.medusajs.com/references/medusa-workflows/steps/attachInventoryItemToVariants/index.html.md) -- [createInventoryItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInventoryItemsStep/index.html.md) -- [createInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInventoryLevelsStep/index.html.md) -- [adjustInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/adjustInventoryLevelsStep/index.html.md) -- [deleteInventoryItemStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInventoryItemStep/index.html.md) -- [deleteInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInventoryLevelsStep/index.html.md) -- [updateInventoryItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryItemsStep/index.html.md) -- [updateInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryLevelsStep/index.html.md) -- [validateInventoryDeleteStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryDeleteStep/index.html.md) -- [validateInventoryItemsForCreate](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryItemsForCreate/index.html.md) -- [validateInventoryLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryLocationsStep/index.html.md) +- [deleteFilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteFilesStep/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) -- [calculateShippingOptionsPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/calculateShippingOptionsPricesStep/index.html.md) - [createFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/createFulfillmentStep/index.html.md) - [createServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createServiceZonesStep/index.html.md) - [createReturnFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnFulfillmentStep/index.html.md) -- [createShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingOptionRulesStep/index.html.md) - [createShippingOptionsPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingOptionsPriceSetsStep/index.html.md) - [deleteFulfillmentSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteFulfillmentSetsStep/index.html.md) - [createShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingProfilesStep/index.html.md) -- [deleteShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingOptionRulesStep/index.html.md) +- [createShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingOptionRulesStep/index.html.md) - [deleteServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteServiceZonesStep/index.html.md) - [deleteShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingOptionsStep/index.html.md) - [setShippingOptionsPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/setShippingOptionsPricesStep/index.html.md) +- [deleteShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingOptionRulesStep/index.html.md) - [updateFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateFulfillmentStep/index.html.md) -- [updateShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingOptionRulesStep/index.html.md) - [updateServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateServiceZonesStep/index.html.md) +- [updateShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingOptionRulesStep/index.html.md) - [updateShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingProfilesStep/index.html.md) - [upsertShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/upsertShippingOptionsStep/index.html.md) -- [validateShipmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShipmentStep/index.html.md) -- [createInviteStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInviteStep/index.html.md) - [validateShippingOptionPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShippingOptionPricesStep/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) +- [validateShipmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShipmentStep/index.html.md) +- [attachInventoryItemToVariants](https://docs.medusajs.com/references/medusa-workflows/steps/attachInventoryItemToVariants/index.html.md) +- [adjustInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/adjustInventoryLevelsStep/index.html.md) +- [createInventoryItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInventoryItemsStep/index.html.md) +- [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) +- [createInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInventoryLevelsStep/index.html.md) +- [updateInventoryItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryItemsStep/index.html.md) +- [updateInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryLevelsStep/index.html.md) +- [validateInventoryDeleteStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryDeleteStep/index.html.md) +- [validateInventoryLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryLocationsStep/index.html.md) +- [validateInventoryItemsForCreate](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryItemsForCreate/index.html.md) +- [updateLineItemsStepWithSelector](https://docs.medusajs.com/references/medusa-workflows/steps/updateLineItemsStepWithSelector/index.html.md) - [listLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listLineItemsStep/index.html.md) - [deleteLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteLineItemsStep/index.html.md) -- [updateLineItemsStepWithSelector](https://docs.medusajs.com/references/medusa-workflows/steps/updateLineItemsStepWithSelector/index.html.md) - [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) +- [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) - [archiveOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/archiveOrdersStep/index.html.md) -- [cancelOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderChangeStep/index.html.md) -- [cancelOrderClaimStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderClaimStep/index.html.md) +- [addOrderTransactionStep](https://docs.medusajs.com/references/medusa-workflows/steps/addOrderTransactionStep/index.html.md) - [cancelOrderExchangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderExchangeStep/index.html.md) +- [cancelOrderClaimStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderClaimStep/index.html.md) +- [cancelOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderChangeStep/index.html.md) - [cancelOrderFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderFulfillmentStep/index.html.md) +- [completeOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/completeOrdersStep/index.html.md) - [cancelOrderReturnStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderReturnStep/index.html.md) - [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) -- [completeOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/completeOrdersStep/index.html.md) -- [createOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderChangeStep/index.html.md) -- [createOrderClaimsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderClaimsStep/index.html.md) -- [createOrderExchangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderExchangesStep/index.html.md) - [createOrderClaimItemsFromActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderClaimItemsFromActionsStep/index.html.md) +- [createOrderClaimsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderClaimsStep/index.html.md) +- [createOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderChangeStep/index.html.md) - [createOrderExchangeItemsFromActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderExchangeItemsFromActionsStep/index.html.md) -- [createOrderLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderLineItemsStep/index.html.md) +- [createOrderExchangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderExchangesStep/index.html.md) - [createOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrdersStep/index.html.md) +- [createOrderLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderLineItemsStep/index.html.md) - [createReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnsStep/index.html.md) -- [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) +- [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) -- [registerOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderChangesStep/index.html.md) -- [previewOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/previewOrderChangeStep/index.html.md) -- [deleteReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReturnsStep/index.html.md) - [deleteOrderShippingMethods](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderShippingMethods/index.html.md) +- [deleteReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReturnsStep/index.html.md) +- [registerOrderFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderFulfillmentStep/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) +- [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) -- [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) -- [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) - [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) +- [updateOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderChangesStep/index.html.md) - [updateOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrdersStep/index.html.md) -- [authorizePaymentSessionStep](https://docs.medusajs.com/references/medusa-workflows/steps/authorizePaymentSessionStep/index.html.md) -- [cancelPaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelPaymentStep/index.html.md) -- [capturePaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/capturePaymentStep/index.html.md) -- [refundPaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/refundPaymentStep/index.html.md) -- [refundPaymentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/refundPaymentsStep/index.html.md) -- [createPriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceListsStep/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) - [createPriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceListPricesStep/index.html.md) +- [createPriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceListsStep/index.html.md) - [deletePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePriceListsStep/index.html.md) -- [updatePriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceListPricesStep/index.html.md) - [removePriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/removePriceListPricesStep/index.html.md) - [getExistingPriceListsPriceIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getExistingPriceListsPriceIdsStep/index.html.md) -- [updatePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceListsStep/index.html.md) +- [updatePriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceListPricesStep/index.html.md) - [validatePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validatePriceListsStep/index.html.md) - [validateVariantPriceLinksStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateVariantPriceLinksStep/index.html.md) +- [updatePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceListsStep/index.html.md) +- [cancelPaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelPaymentStep/index.html.md) +- [authorizePaymentSessionStep](https://docs.medusajs.com/references/medusa-workflows/steps/authorizePaymentSessionStep/index.html.md) +- [capturePaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/capturePaymentStep/index.html.md) +- [refundPaymentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/refundPaymentsStep/index.html.md) +- [refundPaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/refundPaymentStep/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) +- [updatePriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceSetsStep/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) - [createPaymentSessionStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentSessionStep/index.html.md) +- [createRefundReasonStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRefundReasonStep/index.html.md) - [deletePaymentSessionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePaymentSessionsStep/index.html.md) - [deleteRefundReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteRefundReasonsStep/index.html.md) - [updatePaymentCollectionStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePaymentCollectionStep/index.html.md) - [updateRefundReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRefundReasonsStep/index.html.md) - [validateDeletedPaymentSessionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateDeletedPaymentSessionsStep/index.html.md) -- [createPricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPricePreferencesStep/index.html.md) -- [createPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceSetsStep/index.html.md) -- [deletePricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePricePreferencesStep/index.html.md) -- [updatePricePreferencesAsArrayStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePricePreferencesAsArrayStep/index.html.md) -- [updatePricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePricePreferencesStep/index.html.md) -- [updatePriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceSetsStep/index.html.md) -- [batchLinkProductsToCategoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/batchLinkProductsToCategoryStep/index.html.md) - [batchLinkProductsToCollectionStep](https://docs.medusajs.com/references/medusa-workflows/steps/batchLinkProductsToCollectionStep/index.html.md) +- [batchLinkProductsToCategoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/batchLinkProductsToCategoryStep/index.html.md) - [createCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCollectionsStep/index.html.md) -- [createProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductTagsStep/index.html.md) - [createProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductOptionsStep/index.html.md) - [createProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductTypesStep/index.html.md) - [createProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductVariantsStep/index.html.md) +- [createProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductTagsStep/index.html.md) - [createProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductsStep/index.html.md) - [deleteCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCollectionsStep/index.html.md) - [deleteProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductOptionsStep/index.html.md) - [createVariantPricingLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/createVariantPricingLinkStep/index.html.md) - [deleteProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductTagsStep/index.html.md) -- [deleteProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductTypesStep/index.html.md) - [deleteProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductVariantsStep/index.html.md) - [deleteProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductsStep/index.html.md) +- [deleteProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductTypesStep/index.html.md) - [generateProductCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/generateProductCsvStep/index.html.md) -- [getAllProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getAllProductsStep/index.html.md) - [getProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getProductsStep/index.html.md) -- [groupProductsForBatchStep](https://docs.medusajs.com/references/medusa-workflows/steps/groupProductsForBatchStep/index.html.md) +- [getAllProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getAllProductsStep/index.html.md) - [getVariantAvailabilityStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantAvailabilityStep/index.html.md) -- [parseProductCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/parseProductCsvStep/index.html.md) +- [groupProductsForBatchStep](https://docs.medusajs.com/references/medusa-workflows/steps/groupProductsForBatchStep/index.html.md) - [updateCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCollectionsStep/index.html.md) +- [parseProductCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/parseProductCsvStep/index.html.md) - [updateProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductOptionsStep/index.html.md) -- [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) - [updateProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductsStep/index.html.md) +- [updateProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductTypesStep/index.html.md) - [waitConfirmationProductImportStep](https://docs.medusajs.com/references/medusa-workflows/steps/waitConfirmationProductImportStep/index.html.md) -- [updateProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductCategoriesStep/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) - [addCampaignPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/addCampaignPromotionsStep/index.html.md) -- [addRulesToPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/addRulesToPromotionsStep/index.html.md) - [createCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCampaignsStep/index.html.md) +- [addRulesToPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/addRulesToPromotionsStep/index.html.md) - [createPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPromotionsStep/index.html.md) - [deleteCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCampaignsStep/index.html.md) - [deletePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePromotionsStep/index.html.md) - [removeCampaignPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeCampaignPromotionsStep/index.html.md) -- [updateCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCampaignsStep/index.html.md) - [removeRulesFromPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeRulesFromPromotionsStep/index.html.md) +- [updateCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCampaignsStep/index.html.md) - [updatePromotionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePromotionRulesStep/index.html.md) - [updatePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePromotionsStep/index.html.md) - [createRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRegionsStep/index.html.md) - [setRegionsPaymentProvidersStep](https://docs.medusajs.com/references/medusa-workflows/steps/setRegionsPaymentProvidersStep/index.html.md) - [deleteRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteRegionsStep/index.html.md) - [updateRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRegionsStep/index.html.md) -- [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) -- [createReturnReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnReasonsStep/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) - [deleteReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReservationsStep/index.html.md) - [deleteReservationsByLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReservationsByLineItemsStep/index.html.md) -- [updateReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReservationsStep/index.html.md) +- [createReturnReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnReasonsStep/index.html.md) +- [deleteReturnReasonStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReturnReasonStep/index.html.md) +- [updateReturnReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnReasonsStep/index.html.md) - [associateLocationsWithSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/associateLocationsWithSalesChannelsStep/index.html.md) -- [canDeleteSalesChannelsOrThrowStep](https://docs.medusajs.com/references/medusa-workflows/steps/canDeleteSalesChannelsOrThrowStep/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) - [deleteSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteSalesChannelsStep/index.html.md) - [detachLocationsFromSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/detachLocationsFromSalesChannelsStep/index.html.md) +- [updateSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateSalesChannelsStep/index.html.md) - [createSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createSalesChannelsStep/index.html.md) - [detachProductsFromSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/detachProductsFromSalesChannelsStep/index.html.md) -- [updateSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateSalesChannelsStep/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) -- [listShippingOptionsForContextStep](https://docs.medusajs.com/references/medusa-workflows/steps/listShippingOptionsForContextStep/index.html.md) +- [createTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRatesStep/index.html.md) - [createTaxRateRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRateRulesStep/index.html.md) - [createTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRegionsStep/index.html.md) - [deleteTaxRateRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRateRulesStep/index.html.md) -- [createTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRatesStep/index.html.md) - [deleteTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRatesStep/index.html.md) -- [getItemTaxLinesStep](https://docs.medusajs.com/references/medusa-workflows/steps/getItemTaxLinesStep/index.html.md) - [deleteTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRegionsStep/index.html.md) - [listTaxRateIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listTaxRateIdsStep/index.html.md) - [listTaxRateRuleIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listTaxRateRuleIdsStep/index.html.md) +- [getItemTaxLinesStep](https://docs.medusajs.com/references/medusa-workflows/steps/getItemTaxLinesStep/index.html.md) - [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) - [createUsersStep](https://docs.medusajs.com/references/medusa-workflows/steps/createUsersStep/index.html.md) @@ -30050,22 +30178,6 @@ By default, the Medusa Admin is built to the `.medusa/server/public/admin` direc If you want a separate build to host the admin standalone, such as on Vercel, pass the `--admin-only` option as explained in the [Options](#options) section. This outputs the admin to the `.medusa/admin` directory instead. -# develop Command - Medusa CLI Reference - -Start Medusa application in development. This command watches files for any changes, then rebuilds the files and restarts the Medusa application. - -```bash -npx medusa develop -``` - -## Options - -|Option|Description|Default| -|---|---|---|---|---| -|\`-H \\`|Set host of the Medusa server.|\`localhost\`| -|\`-p \\`|Set port of the Medusa server.|\`9000\`| - - # db Commands - Medusa CLI Reference Commands starting with `db:` perform actions on the database. @@ -30186,6 +30298,22 @@ npx medusa db:sync-links |\`--execute-all\`|Skip prompts when syncing links and execute all (including unsafe) actions.|No|Prompts are shown for unsafe actions, by default.| +# 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. @@ -30215,44 +30343,12 @@ medusa new [ []] |\`--db-host \\`|The database host to use for database setup.| -# exec Command - Medusa CLI Reference +# develop 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). +Start Medusa application in development. This command watches files for any changes, then rebuilds the files and restarts the Medusa application. ```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| - - -# telemetry Command - Medusa CLI Reference - -Enable or disable the collection of anonymous data usage. If no option is provided, the command enables the collection of anonymous data usage. - -```bash -npx medusa telemetry -``` - -#### Options - -|Option|Description| -|---|---|---| -|\`--enable\`|Enable telemetry (default).| -|\`--disable\`|Disable telemetry.| - - -# start Command - Medusa CLI Reference - -Start the Medusa application in production. - -```bash -npx medusa start +npx medusa develop ``` ## Options @@ -30261,7 +30357,6 @@ npx medusa start |---|---|---|---|---| |\`-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 @@ -30325,6 +30420,39 @@ npx medusa plugin:build ``` +# telemetry Command - Medusa CLI Reference + +Enable or disable the collection of anonymous data usage. If no option is provided, the command enables the collection of anonymous data usage. + +```bash +npx medusa telemetry +``` + +#### Options + +|Option|Description| +|---|---|---| +|\`--enable\`|Enable telemetry (default).| +|\`--disable\`|Disable telemetry.| + + +# start Command - Medusa CLI Reference + +Start the Medusa application in production. + +```bash +npx medusa start +``` + +## Options + +|Option|Description|Default| +|---|---|---|---|---| +|\`-H \\`|Set host of the Medusa server.|\`localhost\`| +|\`-p \\`|Set port of the Medusa server.|\`9000\`| +|\`--cluster \\`|Start Medusa's Node.js server in |Cluster mode is disabled by default. If the option is passed but no number is passed, Medusa will try to consume all available CPU cores.| + + # user Command - Medusa CLI Reference Create a new admin user. @@ -30581,33 +30709,20 @@ npx medusa exec [file] [args...] |\`args\`|A list of arguments to pass to the function. These arguments are passed in the |No| -# new Command - Medusa CLI Reference +# telemetry 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. +Enable or disable the collection of anonymous data usage. If no option is provided, the command enables the collection of anonymous data usage. ```bash -medusa new [ []] +npx medusa telemetry ``` -## 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 +#### 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.| +|\`--enable\`|Enable telemetry (default).| +|\`--disable\`|Disable telemetry.| # start Command - Medusa CLI Reference @@ -30627,41 +30742,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.| -# 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.| - - # 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. @@ -30723,6 +30803,54 @@ npx medusa plugin:build ``` +# 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\`| + + +# 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.| + + # Medusa JS SDK In this documentation, you'll learn how to install and use Medusa's JS SDK. @@ -35173,6 +35301,795 @@ const user = await userModuleService.createUsers({ ``` +# Implement Custom Line Item Pricing in Medusa + +In this guide, you'll learn how to add line items with custom prices to a cart 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 managing carts and adding line items to them. + +By default, you can add product variants to the cart, where the price of its associated line item is based on the product variant's price. However, you can build customizations to add line items with custom prices to the cart. This is useful when integrating an Enterprise Resource Planning (ERP), Product Information Management (PIM), or other third-party services that provide real-time prices for your products. + +To showcase how to add line items with custom prices to the cart, this guide uses [GoldAPI.io](https://www.goldapi.io) as an example of a third-party system that you can integrate for real-time prices. You can follow the same approach for other third-party integrations that provide custom pricing. + +You can follow this guide whether you're new to Medusa or an advanced Medusa developer. + +### Summary + +This guide will teach you how to: + +- Install and set up Medusa. +- Integrate the third-party service [GoldAPI.io](https://www.goldapi.io) that retrieves real-time prices for metals like Gold and Silver. +- Add an API route to add a product variant that has metals, such as a gold ring, to the cart with the real-time price retrieved from the third-party service. + +![Diagram showcasing overview of implementation for adding an item to cart from storefront.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738920014/Medusa%20Resources/custom-line-item-3_zu3qh2.jpg) + +- [Custom Item Price Repository](https://github.com/medusajs/examples/tree/main/custom-item-price): Find the full code for this guide in this repository. +- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1738246728/OpenApi/Custom_Item_Price_gdfnl3.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. 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). + +Afterwards, 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). Learn more about Medusa's architecture in [this documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md). + +Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's 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: Integrate GoldAPI.io + +### Prerequisites + +- [GoldAPI.io Account. You can create a free account.](https://www.goldapi.io) + +To integrate third-party services into Medusa, you create a custom module. A module is a reusable package with functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup. + +In this step, you'll create a Metal Price Module that uses the GoldAPI.io service to retrieve real-time prices for metals like Gold and Silver. You'll use this module later to retrieve the real-time price of a product variant based on the metals in it, and add it to the cart with that custom price. + +Learn more about modules in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). + +### Create Module Directory + +A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/metal-prices`. + +![Diagram showcasing the module directory to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1738247192/Medusa%20Resources/custom-item-price-1_q16evr.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 the database, which is useful if your module defines tables in the database, or connect to a third-party service. + +In this section, you'll create the Metal Prices Module's service that connects to the GoldAPI.io service to retrieve real-time prices for metals. + +Start by creating the file `src/modules/metal-prices/service.ts` with the following content: + +![Diagram showcasing the service file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1738247303/Medusa%20Resources/custom-item-price-2_eaefis.jpg) + +```ts title="src/modules/metal-prices/service.ts" +type Options = { + accessToken: string + sandbox?: boolean +} + +export default class MetalPricesModuleService { + protected options_: Options + + constructor({}, options: Options) { + this.options_ = options + } +} +``` + +A module can accept options that are passed to its service. You define an `Options` type that indicates the options the module accepts. It accepts two options: + +- `accessToken`: The access token for the GoldAPI.io service. +- `sandbox`: A boolean that indicates whether to simulate sending requests to the GoldAPI.io service. This is useful when running in a test environment. + +The service's constructor receives the module's options as a second parameter. You store the options in the service's `options_` property. + +A module has a container of Medusa Framework tools and local resources in the module that you can access in the service constructor's first parameter. Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md). + +#### Add Method to Retrieve Metal Prices + +Next, you'll add the method to retrieve the metal prices from the third-party service. + +First, add the following types at the beginning of `src/modules/metal-prices/service.ts`: + +```ts title="src/modules/metal-prices/service.ts" +export enum MetalSymbols { + Gold = "XAU", + Silver = "XAG", + Platinum = "XPT", + Palladium = "XPD" +} + +export type PriceResponse = { + metal: MetalSymbols + currency: string + exchange: string + symbol: string + price: number + [key: string]: unknown +} + +``` + +The `MetalSymbols` enum defines the symbols for metals like Gold, Silver, Platinum, and Palladium. The `PriceResponse` type defines the structure of the response from the GoldAPI.io's endpoint. + +Next, add the method `getMetalPrices` to the `MetalPricesModuleService` class: + +```ts title="src/modules/metal-prices/service.ts" +import { MedusaError } from "@medusajs/framework/utils" + +// ... + +export default class MetalPricesModuleService { + // ... + async getMetalPrice( + symbol: MetalSymbols, + currency: string + ): Promise { + const upperCaseSymbol = symbol.toUpperCase() + const upperCaseCurrency = currency.toUpperCase() + + return fetch(`https://www.goldapi.io/api/${upperCaseSymbol}/${upperCaseCurrency}`, { + headers: { + "x-access-token": this.options_.accessToken, + "Content-Type": "application/json", + }, + redirect: "follow", + }).then((response) => response.json()) + .then((response) => { + if (response.error) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + response.error + ) + } + + return response + }) + } +} +``` + +The `getMetalPrice` method accepts the metal symbol and currency as parameters. You send a request to GoldAPI.io's `/api/{symbol}/{currency}` endpoint to retrieve the metal's price, also passing the access token in the request's headers. + +If the response contains an error, you throw a `MedusaError` with the error message. Otherwise, you return the response, which is of type `PriceResponse`. + +#### Add Helper Methods + +You'll also add two helper methods to the `MetalPricesModuleService`. The first one is `getMetalSymbols` that returns the metal symbols as an array of strings: + +```ts title="src/modules/metal-prices/service.ts" +export default class MetalPricesModuleService { + // ... + async getMetalSymbols(): Promise { + return Object.values(MetalSymbols) + } +} +``` + +The second is `getMetalSymbol` that receives a name like `gold` and returns the corresponding metal symbol: + +```ts title="src/modules/metal-prices/service.ts" +export default class MetalPricesModuleService { + // ... + async getMetalSymbol(name: string): Promise { + const formattedName = name.charAt(0).toUpperCase() + name.slice(1).toLowerCase() + return MetalSymbols[formattedName as keyof typeof MetalSymbols] + } +} +``` + +You'll use these methods in later steps. + +### 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/metal-prices/index.ts` with the following content: + +![The directory structure of the Metal Prices Module after adding the definition file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738248049/Medusa%20Resources/custom-item-price-3_imtbuw.jpg) + +```ts title="src/modules/metal-prices/index.ts" +import { Module } from "@medusajs/framework/utils" +import MetalPricesModuleService from "./service" + +export const METAL_PRICES_MODULE = "metal-prices" + +export default Module(METAL_PRICES_MODULE, { + service: MetalPricesModuleService, +}) +``` + +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 `metal-prices`. +2. An object with a required property `service` indicating the module's service. + +### 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/metal-prices", + options: { + accessToken: process.env.GOLD_API_TOKEN, + sandbox: process.env.GOLD_API_SANDBOX === "true", + }, + }, + ], +}) +``` + +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. + +The object also has an `options` property that accepts the module's options. You set the `accessToken` and `sandbox` options based on environment variables. + +You'll find the access token at the top of your GoldAPI.io dashboard. + +![The access token is below the "API Token" header of your GoldAPI.io dashboard.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738248335/Medusa%20Resources/Screenshot_2025-01-30_at_4.44.07_PM_xht3j4.png) + +Set the access token as an environment variable in `.env`: + +```bash +GOLD_API_TOKEN= +``` + +You'll start using the module in the next steps. + +*** + +## Step 3: Add Custom Item to Cart Workflow + +In this section, you'll implement the logic to retrieve the real-time price of a variant based on the metals in it, then add the variant to the cart with the custom price. You'll implement this logic in 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 endpoint. + +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) + +The workflow you'll implement in this section has the following steps: + +- [useQueryGraphStep (Retrieve Cart)](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's ID and currency using Query. +- [useQueryGraphStep (Retrieve Variant)](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the variant's details using Query +- [getVariantMetalPricesStep](#getvariantmetalpricesstep): Retrieve the variant's price using the third-party service. +- [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md): Add the item with the custom price to the cart. +- [useQueryGraphStep (Retrieve Cart)](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the updated cart's details using Query. + +`useQueryGraphStep` and `addToCartWorkflow` are available through Medusa's core workflows package. You'll only implement the `getVariantMetalPricesStep`. + +### getVariantMetalPricesStep + +The `getVariantMetalPricesStep` will retrieve the real-time metal price of a variant received as an input. + +To create the step, create the file `src/workflows/steps/get-variant-metal-prices.ts` with the following content: + +![The directory structure after adding the step file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738249036/Medusa%20Resources/custom-item-price-4_kumzdc.jpg) + +```ts title="src/workflows/steps/get-variant-metal-prices.ts" +import { createStep } from "@medusajs/framework/workflows-sdk" +import { ProductVariantDTO } from "@medusajs/framework/types" +import { METAL_PRICES_MODULE } from "../../modules/metal-prices" +import MetalPricesModuleService from "../../modules/metal-prices/service" + +export type GetVariantMetalPricesStepInput = { + variant: ProductVariantDTO & { + calculated_price?: { + calculated_amount: number + } + } + currencyCode: string + quantity?: number +} + +export const getVariantMetalPricesStep = createStep( + "get-variant-metal-prices", + async ({ + variant, + currencyCode, + quantity = 1, + }: GetVariantMetalPricesStepInput, { container }) => { + const metalPricesModuleService: MetalPricesModuleService = + container.resolve(METAL_PRICES_MODULE) + + // TODO + } +) +``` + +You create a step with `createStep` from the Workflows SDK. It accepts two parameters: + +1. The step's unique name, which is `get-variant-metal-prices`. +2. An async function that receives two parameters: + - An input object with the variant, currency code, and quantity. The variant has a `calculated_price` property that holds the variant's fixed price in the Medusa application. This is useful when you want to add a fixed price to the real-time custom price, such as handling fees. + - 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, so far you only resolve the Metal Prices Module's service from the Medusa container. + +Next, you'll validate that the specified variant can have its price calculated. Add the following import at the top of the file: + +```ts title="src/workflows/steps/get-variant-metal-prices.ts" +import { MedusaError } from "@medusajs/framework/utils" +``` + +And replace the `TODO` in the step function with the following: + +```ts title="src/workflows/steps/get-variant-metal-prices.ts" +const variantMetal = variant.options.find( + (option) => option.option?.title === "Metal" +)?.value +const metalSymbol = await metalPricesModuleService + .getMetalSymbol(variantMetal || "") + +if (!metalSymbol) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Variant doesn't have metal. Make sure the variant's SKU matches a metal symbol." + ) +} + +if (!variant.weight) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Variant doesn't have weight. Make sure the variant has weight to calculate its price." + ) +} + +// TODO retrieve custom price +``` + +In the code above, you first retrieve the metal option's value from the variant's options, assuming that a variant has metals if it has a `Metal` option. Then, you retrieve the metal symbol of the option's value using the `getMetalSymbol` method of the Metal Prices Module's service. + +If the variant doesn't have a metal in its options, the option's value is not valid, or the variant doesn't have a weight, you throw an error. The weight is necessary to calculate the price based on the metal's price per weight. + +Next, you'll retrieve the real-time price of the metal using the third-party service. Replace the `TODO` with the following: + +```ts title="src/workflows/steps/get-variant-metal-prices.ts" +let price = variant.calculated_price?.calculated_amount || 0 +const weight = variant.weight +const { price: metalPrice } = await metalPricesModuleService.getMetalPrice( + metalSymbol as MetalSymbols, currencyCode +) +price += (metalPrice * weight * quantity) + +return new StepResponse(price) +``` + +In the code above, you first set the price to the variant's fixed price, if it has one. Then, you retrieve the metal's price using the `getMetalPrice` method of the Metal Prices Module's service. + +Finally, you calculate the price by multiplying the metal's price by the variant's weight and the quantity to add to the cart, then add the fixed price to it. + +Every step must return a `StepResponse` instance. The `StepResponse` constructor accepts the step's output as a parameter, which in this case is the variant's price. + +### Create addCustomToCartWorkflow + +Now that you have the `getVariantMetalPricesStep`, you can create the workflow that adds the item with custom pricing to the cart. + +Create the file `src/workflows/add-custom-to-cart.ts` with the following content: + +![The directory structure after adding the workflow file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738251380/Medusa%20Resources/custom-item-price-5_zorahv.jpg) + +```ts title="src/workflows/add-custom-to-cart.ts" highlights={workflowHighlights} +import { createWorkflow } from "@medusajs/framework/workflows-sdk" +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +import { QueryContext } from "@medusajs/framework/utils" + +type AddCustomToCartWorkflowInput = { + cart_id: string + item: { + variant_id: string + quantity: number + metadata?: Record + } +} + +export const addCustomToCartWorkflow = createWorkflow( + "add-custom-to-cart", + ({ cart_id, item }: AddCustomToCartWorkflowInput) => { + // @ts-ignore + const { data: carts } = useQueryGraphStep({ + entity: "cart", + filters: { id: cart_id }, + fields: ["id", "currency_code"], + }) + + const { data: variants } = useQueryGraphStep({ + entity: "variant", + fields: [ + "*", + "options.*", + "options.option.*", + "calculated_price.*", + ], + filters: { + id: item.variant_id, + }, + options: { + throwIfKeyNotFound: true, + }, + context: { + calculated_price: QueryContext({ + currency_code: carts[0].currency_code, + }), + }, + }).config({ name: "retrieve-variant" }) + + // TODO add more steps + } +) +``` + +You create a workflow with `createWorkflow` from the Workflows SDK. It accepts two parameters: + +1. The workflow's unique name, which is `add-custom-to-cart`. +2. A function that receives an input object with the cart's ID and the item to add to the cart. The item has the variant's ID, quantity, and optional metadata. + +In the function, you first retrieve the cart's details using the `useQueryGraphStep` helper step. This step uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) which is a Modules SDK tool that retrieves data across modules. You use it to retrieve the cart's ID and currency code. + +You also retrieve the variant's details using the `useQueryGraphStep` helper step. You pass the variant's ID to the step's filters and specify the fields to retrieve. To retrieve the variant's price based on the cart's context, you pass the cart's currency code to the `calculated_price` context. + +Next, you'll retrieve the variant's real-time price using the `getVariantMetalPricesStep` you created earlier. First, add the following import: + +```ts title="src/workflows/add-custom-to-cart.ts" +import { + getVariantMetalPricesStep, + GetVariantMetalPricesStepInput, +} from "./steps/get-variant-metal-prices" +``` + +Then, replace the `TODO` in the workflow with the following: + +```ts title="src/workflows/add-custom-to-cart.ts" +const price = getVariantMetalPricesStep({ + variant: variants[0], + currencyCode: carts[0].currency_code, + quantity: item.quantity, +} as unknown as GetVariantMetalPricesStepInput) + +// TODO add item with custom price to cart +``` + +You execute the `getVariantMetalPricesStep` passing it the variant's details, the cart's currency code, and the quantity of the item to add to the cart. The step returns the variant's custom price. + +Next, you'll add the item with the custom price to the cart. First, add the following imports at the top of the file: + +```ts title="src/workflows/add-custom-to-cart.ts" +import { transform } from "@medusajs/framework/workflows-sdk" +import { addToCartWorkflow } from "@medusajs/medusa/core-flows" +``` + +Then, replace the `TODO` in the workflow with the following: + +```ts title="src/workflows/add-custom-to-cart.ts" +const itemToAdd = transform({ + item, + price, +}, (data) => { + return [{ + ...data.item, + unit_price: data.price, + }] +}) + +addToCartWorkflow.runAsStep({ + input: { + items: itemToAdd, + cart_id, + }, +}) + +// TODO retrieve and return cart +``` + +You prepare the item to add to the cart using `transform` from the Workflows SDK. It allows you to manipulate and create variables in a workflow. After that, you use Medusa's `addToCartWorkflow` to add the item with the custom price to the cart. + +A workflow's constructor function has some constraints in implementation, which is why you need to use `transform` for variable manipulation. Learn more about these constraints in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md). + +Lastly, you'll retrieve the cart's details again and return them. Add the following import at the beginning of the file: + +```ts title="src/workflows/add-custom-to-cart.ts" +import { WorkflowResponse } from "@medusajs/framework/workflows-sdk" +``` + +And replace the last `TODO` in the workflow with the following: + +```ts title="src/workflows/add-custom-to-cart.ts" +// @ts-ignore +const { data: updatedCarts } = useQueryGraphStep({ + entity: "cart", + filters: { id: cart_id }, + fields: ["id", "items.*"], +}).config({ name: "refetch-cart" }) + +return new WorkflowResponse({ + cart: updatedCarts[0], +}) +``` + +In the code above, you retrieve the updated cart's details using the `useQueryGraphStep` helper step. To return data from the workflow, you create and return a `WorkflowResponse` instance. It accepts as a parameter the data to return, which is the updated cart. + +In the next step, you'll use the workflow in a custom route to add an item with a custom price to the cart. + +*** + +## Step 4: Create Add Custom Item to Cart API Route + +Now that you've implemented the logic to add an item with a custom price to the cart, you'll expose this functionality in an API route. + +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/carts/:id/line-items-metals` that executes the workflow from the previous step to add a product variant with custom price to the cart. + +Learn more about API routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). + +### Create API Route + +An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. + +The path of the API route is the file's path relative to `src/api`. So, to create the `/store/carts/:id/line-items-metals` API route, create the file `src/api/store/carts/[id]/line-items-metals/route.ts` with the following content: + +![The directory structure after adding the API route file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738252712/Medusa%20Resources/custom-item-price-6_deecbu.jpg) + +```ts title="src/api/store/carts/[id]/line-items-metals/route.ts" +import { MedusaRequest, MedusaResponse } from "@medusajs/framework" +import { HttpTypes } from "@medusajs/framework/types" +import { addCustomToCartWorkflow } from "../../../../../workflows/add-custom-to-cart" + +export const POST = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const { id } = req.params + const item = req.validatedBody + + const { result } = await addCustomToCartWorkflow(req.scope) + .run({ + input: { + cart_id: id, + item, + }, + }) + + res.status(200).json({ cart: result.cart }) +} +``` + +Since you export a `POST` function in this file, you're exposing a `POST` API route at `/store/carts/:id/line-items-metals`. The route handler function accepts two parameters: + +1. A request object with details and context on the request, such as path and body parameters. +2. A response object to manipulate and send the response. + +In the function, you retrieve the cart's ID from the path parameter, and the item's details from the request body. This API route will accept the same request body parameters as Medusa's [Add Item to Cart API Route](https://docs.medusajs.com/api/store#carts_postcartsidlineitems). + +Then, you execute the `addCustomToCartWorkflow` by invoking it, passing it the Medusa container, which is available in the request's `scope` property, then executing its `run` method. You pass the workflow's input object with the cart's ID and the item to add to the cart. + +Finally, you return a response with the updated cart's details. + +### Add Request Body Validation Middleware + +To ensure that the request body contains the required parameters, you'll add a middleware that validates the incoming request's body based on a defined schema. + +A middleware is a function executed before the API route when a request is sent to it. You define middlewares in Medusa in the `src/api/middlewares.ts` directory. + +Learn more about middlewares in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md). + +To add a validation middleware to the custom API route, create the file `src/api/middlewares.ts` with the following content: + +![The directory structure after adding the middleware file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738253099/Medusa%20Resources/custom-item-price-7_l7iw2a.jpg) + +```ts title="src/api/middlewares.ts" +import { + defineMiddlewares, + validateAndTransformBody, +} from "@medusajs/framework/http" +import { + StoreAddCartLineItem, +} from "@medusajs/medusa/api/store/carts/validators" + +export default defineMiddlewares({ + routes: [ + { + matcher: "/store/carts/:id/line-items-metals", + method: "POST", + middlewares: [ + validateAndTransformBody( + StoreAddCartLineItem + ), + ], + }, + ], +}) +``` + +In this file, you export the middlewares definition using `defineMiddlewares` from the Medusa Framework. This function accepts an object having a `routes` property, which is an array of middleware configurations to apply on routes. + +You pass in the `routes` array an object having the following properties: + +- `matcher`: The route to apply the middleware on. +- `method`: The HTTP method to apply the middleware on for the specified API route. +- `middlewares`: An array of the middlewares to apply. You apply the `validateAndTransformBody` middleware, which validates the request body based on the `StoreAddCartLineItem` schema. This validation schema is the same schema used for Medusa's [Add Item to Cart API Route](https://docs.medusajs.com/api/store#carts_postcartsidlineitems). + +Any request sent to the `/store/carts/:id/line-items-metals` API route will now fail if it doesn't have the required parameters. + +Learn more about API route validation in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/validation/index.html.md). + +### Prepare to Test API Route + +Before you test the API route, you'll prepare and retrieve the necessary data to add a product variant with a custom price to the cart. + +#### Create Product with Metal Variant + +You'll first create a product that has a `Metal` option, and variant(s) with values for this option. + +Start the Medusa application with the following command: + +```bash npm2yarn +npm run dev +``` + +Then, open the Medusa Admin dashboard at `localhost:9000/app` and log in with the email and password you created when you installed the Medusa application in the first step. + +Once you log in, click on Products in the sidebar, then click the Create button at the top right. + +![Click on Products in the sidebar at the left, then click on the Create button at the top right of the content](https://res.cloudinary.com/dza7lstvk/image/upload/v1738253415/Medusa%20Resources/Screenshot_2025-01-30_at_6.09.36_PM_ee0jr2.png) + +Then, in the Create Product form: + +1. Enter a name for the product, and optionally enter other details like description. +2. Enable the "Yes, this is a product with variants" toggle. +3. Under Product Options, enter "Metal" for the title, and enter "Gold" for the values. + +Once you're done, click the Continue button. + +![Fill in the product details, enable the "Yes, this is a product with variants" toggle, and add the "Metal" option with "Gold" value](https://res.cloudinary.com/dza7lstvk/image/upload/v1738253520/Medusa%20Resources/Screenshot_2025-01-30_at_6.11.29_PM_lqxth9.png) + +You can skip the next two steps by clicking the Continue button again, then the Publish button. + +Once you're done, the product's page will open. You'll now add weight to the product's Gold variant. To do that: + +- Scroll to the Variants section and find the Gold variant. +- Click on the three-dots icon at its right. +- Choose "Edit" from the dropdown. + +![Find the Gold variant in the Variants section, click on the three-dots icon, and choose "Edit"](https://res.cloudinary.com/dza7lstvk/image/upload/v1738254038/Medusa%20Resources/Screenshot_2025-01-30_at_6.19.52_PM_j3hjcx.png) + +In the side window that opens, find the Weight field, enter the weight, and click the Save button. + +![Enter the weight in the Weight field, then click the Save button](https://res.cloudinary.com/dza7lstvk/image/upload/v1738254165/Medusa%20Resources/Screenshot_2025-01-30_at_6.22.15_PM_yplzdp.png) + +Finally, you need to set fixed prices for the variant, even if they're just `0`. To do that: + +1. Click on the three-dots icon at the top right of the Variants section. +2. Choose "Edit Prices" from the dropdown. + +![Click on the three-dots icon at the top right of the Variants section, then choose "Edit Prices"](https://res.cloudinary.com/dza7lstvk/image/upload/v1738255203/Medusa%20Resources/Screenshot_2025-01-30_at_6.39.35_PM_s3jpxh.png) + +For each cell in the table, either enter a fixed price for the specified currency or leave it as `0`. Once you're done, click the Save button. + +![Enter fixed prices for the variant in the table, then click the Save button](https://res.cloudinary.com/dza7lstvk/image/upload/v1738255272/Medusa%20Resources/Screenshot_2025-01-30_at_6.40.45_PM_zw1l59.png) + +You'll use this variant to add it to the cart later. You can find its ID by clicking on the variant, opening its details page. Then, on the details page, click on the icon at the right of the JSON section, and copy the ID from the JSON data. + +![Click on the icon at the right of the JSON section to copy the variant's ID](https://res.cloudinary.com/dza7lstvk/image/upload/v1738254314/Medusa%20Resources/Screenshot_2025-01-30_at_6.24.49_PM_ka7xew.png) + +#### Retrieve Publishable API Key + +All requests sent to API routes starting with `/store` must have a publishable API key in the header. This ensures the request's operations are scoped to the publishable API key's associated sales channels. For example, products that aren't available in a cart's sales channel can't be added to it. + +To retrieve the publishable API key, on the Medusa Admin: + +1. Click on Settings in the sidebar at the bottom left. +2. Click on Publishable API Keys from the sidebar, then click on a publishable API key in the list. + +![Click on publishable API keys in the Settings sidebar, then click on a publishable API key in the list](https://res.cloudinary.com/dza7lstvk/image/upload/v1738254523/Medusa%20Resources/Screenshot_2025-01-30_at_6.28.17_PM_mldscc.png) + +3. Click on the publishable API key to copy it. + +![Click on the publishable API key to copy it](https://res.cloudinary.com/dza7lstvk/image/upload/v1738254601/Medusa%20Resources/Screenshot_2025-01-30_at_6.29.26_PM_vvatki.png) + +You'll use this key when you test the API route. + +### Test API Route + +To test out the API route, you need to create a cart. A cart must be associated with a region. So, to retrieve the ID of a region in your store, send a `GET` request to the `/store/regions` API route: + +```bash +curl 'localhost:9000/store/regions' \ +-H 'x-publishable-api-key: {api_key}' +``` + +Make sure to replace `{api_key}` with the publishable API key you copied earlier. + +This will return a list of regions. Copy the ID of one of the regions. + +Then, send a `POST` request to the `/store/carts` API route to create a cart: + +```bash +curl -X POST 'localhost:9000/store/carts' \ +-H 'x-publishable-api-key: {api_key}' \ +-H 'Content-Type: application/json' \ +--data '{ + "region_id": "{region_id}" +}' +``` + +Make sure to replace `{api_key}` with the publishable API key you copied earlier, and `{region_id}` with the ID of a region from the previous request. + +This will return the created cart. Copy the ID of the cart to use it next. + +Finally, to add the Gold variant to the cart with a custom price, send a `POST` request to the `/store/carts/:id/line-items-metals` API route: + +```bash +curl -X POST 'localhost:9000/store/carts/{cart_id}/line-items-metals' \ +-H 'x-publishable-api-key: {api_key}' \ +-H 'Content-Type: application/json' \ +--data '{ + "variant_id": "{variant_id}", + "quantity": 1 +}' +``` + +Make sure to replace: + +- `{api_key}` with the publishable API key you copied earlier. +- `{cart_id}` with the ID of the cart you created. +- `{variant_id}` with the ID of the Gold variant you created. + +This will return the cart's details, where you can see in its `items` array the item with the custom price: + +```json title="Example Response" +{ + "cart": { + "items": [ + { + "variant_id": "{variant_id}", + "quantity": 1, + "is_custom_price": true, + // example custom price + "unit_price": 2000 + } + ] + } +} +``` + +The price will be the result of the calculation you've implemented earlier, which is the fixed price of the variant plus the real-time price of the metal, multiplied by the weight of the variant and the quantity added to the cart. + +This price will be reflected in the cart's total price, and you can proceed to checkout with the custom-priced item. + +*** + +## Next Steps + +You've now implemented custom item pricing in Medusa. You can also customize the [storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) to use the new API route to add custom-priced items to the cart. + +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 Quote Management in Medusa In this guide, you'll learn how to implement quote management in Medusa. @@ -39054,795 +39971,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 Custom Line Item Pricing in Medusa - -In this guide, you'll learn how to add line items with custom prices to a cart 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 managing carts and adding line items to them. - -By default, you can add product variants to the cart, where the price of its associated line item is based on the product variant's price. However, you can build customizations to add line items with custom prices to the cart. This is useful when integrating an Enterprise Resource Planning (ERP), Product Information Management (PIM), or other third-party services that provide real-time prices for your products. - -To showcase how to add line items with custom prices to the cart, this guide uses [GoldAPI.io](https://www.goldapi.io) as an example of a third-party system that you can integrate for real-time prices. You can follow the same approach for other third-party integrations that provide custom pricing. - -You can follow this guide whether you're new to Medusa or an advanced Medusa developer. - -### Summary - -This guide will teach you how to: - -- Install and set up Medusa. -- Integrate the third-party service [GoldAPI.io](https://www.goldapi.io) that retrieves real-time prices for metals like Gold and Silver. -- Add an API route to add a product variant that has metals, such as a gold ring, to the cart with the real-time price retrieved from the third-party service. - -![Diagram showcasing overview of implementation for adding an item to cart from storefront.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738920014/Medusa%20Resources/custom-line-item-3_zu3qh2.jpg) - -- [Custom Item Price Repository](https://github.com/medusajs/examples/tree/main/custom-item-price): Find the full code for this guide in this repository. -- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1738246728/OpenApi/Custom_Item_Price_gdfnl3.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. 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). - -Afterwards, 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). Learn more about Medusa's architecture in [this documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md). - -Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's 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: Integrate GoldAPI.io - -### Prerequisites - -- [GoldAPI.io Account. You can create a free account.](https://www.goldapi.io) - -To integrate third-party services into Medusa, you create a custom module. A module is a reusable package with functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup. - -In this step, you'll create a Metal Price Module that uses the GoldAPI.io service to retrieve real-time prices for metals like Gold and Silver. You'll use this module later to retrieve the real-time price of a product variant based on the metals in it, and add it to the cart with that custom price. - -Learn more about modules in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). - -### Create Module Directory - -A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/metal-prices`. - -![Diagram showcasing the module directory to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1738247192/Medusa%20Resources/custom-item-price-1_q16evr.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 the database, which is useful if your module defines tables in the database, or connect to a third-party service. - -In this section, you'll create the Metal Prices Module's service that connects to the GoldAPI.io service to retrieve real-time prices for metals. - -Start by creating the file `src/modules/metal-prices/service.ts` with the following content: - -![Diagram showcasing the service file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1738247303/Medusa%20Resources/custom-item-price-2_eaefis.jpg) - -```ts title="src/modules/metal-prices/service.ts" -type Options = { - accessToken: string - sandbox?: boolean -} - -export default class MetalPricesModuleService { - protected options_: Options - - constructor({}, options: Options) { - this.options_ = options - } -} -``` - -A module can accept options that are passed to its service. You define an `Options` type that indicates the options the module accepts. It accepts two options: - -- `accessToken`: The access token for the GoldAPI.io service. -- `sandbox`: A boolean that indicates whether to simulate sending requests to the GoldAPI.io service. This is useful when running in a test environment. - -The service's constructor receives the module's options as a second parameter. You store the options in the service's `options_` property. - -A module has a container of Medusa Framework tools and local resources in the module that you can access in the service constructor's first parameter. Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md). - -#### Add Method to Retrieve Metal Prices - -Next, you'll add the method to retrieve the metal prices from the third-party service. - -First, add the following types at the beginning of `src/modules/metal-prices/service.ts`: - -```ts title="src/modules/metal-prices/service.ts" -export enum MetalSymbols { - Gold = "XAU", - Silver = "XAG", - Platinum = "XPT", - Palladium = "XPD" -} - -export type PriceResponse = { - metal: MetalSymbols - currency: string - exchange: string - symbol: string - price: number - [key: string]: unknown -} - -``` - -The `MetalSymbols` enum defines the symbols for metals like Gold, Silver, Platinum, and Palladium. The `PriceResponse` type defines the structure of the response from the GoldAPI.io's endpoint. - -Next, add the method `getMetalPrices` to the `MetalPricesModuleService` class: - -```ts title="src/modules/metal-prices/service.ts" -import { MedusaError } from "@medusajs/framework/utils" - -// ... - -export default class MetalPricesModuleService { - // ... - async getMetalPrice( - symbol: MetalSymbols, - currency: string - ): Promise { - const upperCaseSymbol = symbol.toUpperCase() - const upperCaseCurrency = currency.toUpperCase() - - return fetch(`https://www.goldapi.io/api/${upperCaseSymbol}/${upperCaseCurrency}`, { - headers: { - "x-access-token": this.options_.accessToken, - "Content-Type": "application/json", - }, - redirect: "follow", - }).then((response) => response.json()) - .then((response) => { - if (response.error) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - response.error - ) - } - - return response - }) - } -} -``` - -The `getMetalPrice` method accepts the metal symbol and currency as parameters. You send a request to GoldAPI.io's `/api/{symbol}/{currency}` endpoint to retrieve the metal's price, also passing the access token in the request's headers. - -If the response contains an error, you throw a `MedusaError` with the error message. Otherwise, you return the response, which is of type `PriceResponse`. - -#### Add Helper Methods - -You'll also add two helper methods to the `MetalPricesModuleService`. The first one is `getMetalSymbols` that returns the metal symbols as an array of strings: - -```ts title="src/modules/metal-prices/service.ts" -export default class MetalPricesModuleService { - // ... - async getMetalSymbols(): Promise { - return Object.values(MetalSymbols) - } -} -``` - -The second is `getMetalSymbol` that receives a name like `gold` and returns the corresponding metal symbol: - -```ts title="src/modules/metal-prices/service.ts" -export default class MetalPricesModuleService { - // ... - async getMetalSymbol(name: string): Promise { - const formattedName = name.charAt(0).toUpperCase() + name.slice(1).toLowerCase() - return MetalSymbols[formattedName as keyof typeof MetalSymbols] - } -} -``` - -You'll use these methods in later steps. - -### 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/metal-prices/index.ts` with the following content: - -![The directory structure of the Metal Prices Module after adding the definition file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738248049/Medusa%20Resources/custom-item-price-3_imtbuw.jpg) - -```ts title="src/modules/metal-prices/index.ts" -import { Module } from "@medusajs/framework/utils" -import MetalPricesModuleService from "./service" - -export const METAL_PRICES_MODULE = "metal-prices" - -export default Module(METAL_PRICES_MODULE, { - service: MetalPricesModuleService, -}) -``` - -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 `metal-prices`. -2. An object with a required property `service` indicating the module's service. - -### 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/metal-prices", - options: { - accessToken: process.env.GOLD_API_TOKEN, - sandbox: process.env.GOLD_API_SANDBOX === "true", - }, - }, - ], -}) -``` - -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. - -The object also has an `options` property that accepts the module's options. You set the `accessToken` and `sandbox` options based on environment variables. - -You'll find the access token at the top of your GoldAPI.io dashboard. - -![The access token is below the "API Token" header of your GoldAPI.io dashboard.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738248335/Medusa%20Resources/Screenshot_2025-01-30_at_4.44.07_PM_xht3j4.png) - -Set the access token as an environment variable in `.env`: - -```bash -GOLD_API_TOKEN= -``` - -You'll start using the module in the next steps. - -*** - -## Step 3: Add Custom Item to Cart Workflow - -In this section, you'll implement the logic to retrieve the real-time price of a variant based on the metals in it, then add the variant to the cart with the custom price. You'll implement this logic in 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 endpoint. - -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) - -The workflow you'll implement in this section has the following steps: - -- [useQueryGraphStep (Retrieve Cart)](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's ID and currency using Query. -- [useQueryGraphStep (Retrieve Variant)](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the variant's details using Query -- [getVariantMetalPricesStep](#getvariantmetalpricesstep): Retrieve the variant's price using the third-party service. -- [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md): Add the item with the custom price to the cart. -- [useQueryGraphStep (Retrieve Cart)](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the updated cart's details using Query. - -`useQueryGraphStep` and `addToCartWorkflow` are available through Medusa's core workflows package. You'll only implement the `getVariantMetalPricesStep`. - -### getVariantMetalPricesStep - -The `getVariantMetalPricesStep` will retrieve the real-time metal price of a variant received as an input. - -To create the step, create the file `src/workflows/steps/get-variant-metal-prices.ts` with the following content: - -![The directory structure after adding the step file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738249036/Medusa%20Resources/custom-item-price-4_kumzdc.jpg) - -```ts title="src/workflows/steps/get-variant-metal-prices.ts" -import { createStep } from "@medusajs/framework/workflows-sdk" -import { ProductVariantDTO } from "@medusajs/framework/types" -import { METAL_PRICES_MODULE } from "../../modules/metal-prices" -import MetalPricesModuleService from "../../modules/metal-prices/service" - -export type GetVariantMetalPricesStepInput = { - variant: ProductVariantDTO & { - calculated_price?: { - calculated_amount: number - } - } - currencyCode: string - quantity?: number -} - -export const getVariantMetalPricesStep = createStep( - "get-variant-metal-prices", - async ({ - variant, - currencyCode, - quantity = 1, - }: GetVariantMetalPricesStepInput, { container }) => { - const metalPricesModuleService: MetalPricesModuleService = - container.resolve(METAL_PRICES_MODULE) - - // TODO - } -) -``` - -You create a step with `createStep` from the Workflows SDK. It accepts two parameters: - -1. The step's unique name, which is `get-variant-metal-prices`. -2. An async function that receives two parameters: - - An input object with the variant, currency code, and quantity. The variant has a `calculated_price` property that holds the variant's fixed price in the Medusa application. This is useful when you want to add a fixed price to the real-time custom price, such as handling fees. - - 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, so far you only resolve the Metal Prices Module's service from the Medusa container. - -Next, you'll validate that the specified variant can have its price calculated. Add the following import at the top of the file: - -```ts title="src/workflows/steps/get-variant-metal-prices.ts" -import { MedusaError } from "@medusajs/framework/utils" -``` - -And replace the `TODO` in the step function with the following: - -```ts title="src/workflows/steps/get-variant-metal-prices.ts" -const variantMetal = variant.options.find( - (option) => option.option?.title === "Metal" -)?.value -const metalSymbol = await metalPricesModuleService - .getMetalSymbol(variantMetal || "") - -if (!metalSymbol) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Variant doesn't have metal. Make sure the variant's SKU matches a metal symbol." - ) -} - -if (!variant.weight) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Variant doesn't have weight. Make sure the variant has weight to calculate its price." - ) -} - -// TODO retrieve custom price -``` - -In the code above, you first retrieve the metal option's value from the variant's options, assuming that a variant has metals if it has a `Metal` option. Then, you retrieve the metal symbol of the option's value using the `getMetalSymbol` method of the Metal Prices Module's service. - -If the variant doesn't have a metal in its options, the option's value is not valid, or the variant doesn't have a weight, you throw an error. The weight is necessary to calculate the price based on the metal's price per weight. - -Next, you'll retrieve the real-time price of the metal using the third-party service. Replace the `TODO` with the following: - -```ts title="src/workflows/steps/get-variant-metal-prices.ts" -let price = variant.calculated_price?.calculated_amount || 0 -const weight = variant.weight -const { price: metalPrice } = await metalPricesModuleService.getMetalPrice( - metalSymbol as MetalSymbols, currencyCode -) -price += (metalPrice * weight * quantity) - -return new StepResponse(price) -``` - -In the code above, you first set the price to the variant's fixed price, if it has one. Then, you retrieve the metal's price using the `getMetalPrice` method of the Metal Prices Module's service. - -Finally, you calculate the price by multiplying the metal's price by the variant's weight and the quantity to add to the cart, then add the fixed price to it. - -Every step must return a `StepResponse` instance. The `StepResponse` constructor accepts the step's output as a parameter, which in this case is the variant's price. - -### Create addCustomToCartWorkflow - -Now that you have the `getVariantMetalPricesStep`, you can create the workflow that adds the item with custom pricing to the cart. - -Create the file `src/workflows/add-custom-to-cart.ts` with the following content: - -![The directory structure after adding the workflow file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738251380/Medusa%20Resources/custom-item-price-5_zorahv.jpg) - -```ts title="src/workflows/add-custom-to-cart.ts" highlights={workflowHighlights} -import { createWorkflow } from "@medusajs/framework/workflows-sdk" -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -import { QueryContext } from "@medusajs/framework/utils" - -type AddCustomToCartWorkflowInput = { - cart_id: string - item: { - variant_id: string - quantity: number - metadata?: Record - } -} - -export const addCustomToCartWorkflow = createWorkflow( - "add-custom-to-cart", - ({ cart_id, item }: AddCustomToCartWorkflowInput) => { - // @ts-ignore - const { data: carts } = useQueryGraphStep({ - entity: "cart", - filters: { id: cart_id }, - fields: ["id", "currency_code"], - }) - - const { data: variants } = useQueryGraphStep({ - entity: "variant", - fields: [ - "*", - "options.*", - "options.option.*", - "calculated_price.*", - ], - filters: { - id: item.variant_id, - }, - options: { - throwIfKeyNotFound: true, - }, - context: { - calculated_price: QueryContext({ - currency_code: carts[0].currency_code, - }), - }, - }).config({ name: "retrieve-variant" }) - - // TODO add more steps - } -) -``` - -You create a workflow with `createWorkflow` from the Workflows SDK. It accepts two parameters: - -1. The workflow's unique name, which is `add-custom-to-cart`. -2. A function that receives an input object with the cart's ID and the item to add to the cart. The item has the variant's ID, quantity, and optional metadata. - -In the function, you first retrieve the cart's details using the `useQueryGraphStep` helper step. This step uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) which is a Modules SDK tool that retrieves data across modules. You use it to retrieve the cart's ID and currency code. - -You also retrieve the variant's details using the `useQueryGraphStep` helper step. You pass the variant's ID to the step's filters and specify the fields to retrieve. To retrieve the variant's price based on the cart's context, you pass the cart's currency code to the `calculated_price` context. - -Next, you'll retrieve the variant's real-time price using the `getVariantMetalPricesStep` you created earlier. First, add the following import: - -```ts title="src/workflows/add-custom-to-cart.ts" -import { - getVariantMetalPricesStep, - GetVariantMetalPricesStepInput, -} from "./steps/get-variant-metal-prices" -``` - -Then, replace the `TODO` in the workflow with the following: - -```ts title="src/workflows/add-custom-to-cart.ts" -const price = getVariantMetalPricesStep({ - variant: variants[0], - currencyCode: carts[0].currency_code, - quantity: item.quantity, -} as unknown as GetVariantMetalPricesStepInput) - -// TODO add item with custom price to cart -``` - -You execute the `getVariantMetalPricesStep` passing it the variant's details, the cart's currency code, and the quantity of the item to add to the cart. The step returns the variant's custom price. - -Next, you'll add the item with the custom price to the cart. First, add the following imports at the top of the file: - -```ts title="src/workflows/add-custom-to-cart.ts" -import { transform } from "@medusajs/framework/workflows-sdk" -import { addToCartWorkflow } from "@medusajs/medusa/core-flows" -``` - -Then, replace the `TODO` in the workflow with the following: - -```ts title="src/workflows/add-custom-to-cart.ts" -const itemToAdd = transform({ - item, - price, -}, (data) => { - return [{ - ...data.item, - unit_price: data.price, - }] -}) - -addToCartWorkflow.runAsStep({ - input: { - items: itemToAdd, - cart_id, - }, -}) - -// TODO retrieve and return cart -``` - -You prepare the item to add to the cart using `transform` from the Workflows SDK. It allows you to manipulate and create variables in a workflow. After that, you use Medusa's `addToCartWorkflow` to add the item with the custom price to the cart. - -A workflow's constructor function has some constraints in implementation, which is why you need to use `transform` for variable manipulation. Learn more about these constraints in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md). - -Lastly, you'll retrieve the cart's details again and return them. Add the following import at the beginning of the file: - -```ts title="src/workflows/add-custom-to-cart.ts" -import { WorkflowResponse } from "@medusajs/framework/workflows-sdk" -``` - -And replace the last `TODO` in the workflow with the following: - -```ts title="src/workflows/add-custom-to-cart.ts" -// @ts-ignore -const { data: updatedCarts } = useQueryGraphStep({ - entity: "cart", - filters: { id: cart_id }, - fields: ["id", "items.*"], -}).config({ name: "refetch-cart" }) - -return new WorkflowResponse({ - cart: updatedCarts[0], -}) -``` - -In the code above, you retrieve the updated cart's details using the `useQueryGraphStep` helper step. To return data from the workflow, you create and return a `WorkflowResponse` instance. It accepts as a parameter the data to return, which is the updated cart. - -In the next step, you'll use the workflow in a custom route to add an item with a custom price to the cart. - -*** - -## Step 4: Create Add Custom Item to Cart API Route - -Now that you've implemented the logic to add an item with a custom price to the cart, you'll expose this functionality in an API route. - -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/carts/:id/line-items-metals` that executes the workflow from the previous step to add a product variant with custom price to the cart. - -Learn more about API routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). - -### Create API Route - -An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. - -The path of the API route is the file's path relative to `src/api`. So, to create the `/store/carts/:id/line-items-metals` API route, create the file `src/api/store/carts/[id]/line-items-metals/route.ts` with the following content: - -![The directory structure after adding the API route file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738252712/Medusa%20Resources/custom-item-price-6_deecbu.jpg) - -```ts title="src/api/store/carts/[id]/line-items-metals/route.ts" -import { MedusaRequest, MedusaResponse } from "@medusajs/framework" -import { HttpTypes } from "@medusajs/framework/types" -import { addCustomToCartWorkflow } from "../../../../../workflows/add-custom-to-cart" - -export const POST = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - const { id } = req.params - const item = req.validatedBody - - const { result } = await addCustomToCartWorkflow(req.scope) - .run({ - input: { - cart_id: id, - item, - }, - }) - - res.status(200).json({ cart: result.cart }) -} -``` - -Since you export a `POST` function in this file, you're exposing a `POST` API route at `/store/carts/:id/line-items-metals`. The route handler function accepts two parameters: - -1. A request object with details and context on the request, such as path and body parameters. -2. A response object to manipulate and send the response. - -In the function, you retrieve the cart's ID from the path parameter, and the item's details from the request body. This API route will accept the same request body parameters as Medusa's [Add Item to Cart API Route](https://docs.medusajs.com/api/store#carts_postcartsidlineitems). - -Then, you execute the `addCustomToCartWorkflow` by invoking it, passing it the Medusa container, which is available in the request's `scope` property, then executing its `run` method. You pass the workflow's input object with the cart's ID and the item to add to the cart. - -Finally, you return a response with the updated cart's details. - -### Add Request Body Validation Middleware - -To ensure that the request body contains the required parameters, you'll add a middleware that validates the incoming request's body based on a defined schema. - -A middleware is a function executed before the API route when a request is sent to it. You define middlewares in Medusa in the `src/api/middlewares.ts` directory. - -Learn more about middlewares in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md). - -To add a validation middleware to the custom API route, create the file `src/api/middlewares.ts` with the following content: - -![The directory structure after adding the middleware file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738253099/Medusa%20Resources/custom-item-price-7_l7iw2a.jpg) - -```ts title="src/api/middlewares.ts" -import { - defineMiddlewares, - validateAndTransformBody, -} from "@medusajs/framework/http" -import { - StoreAddCartLineItem, -} from "@medusajs/medusa/api/store/carts/validators" - -export default defineMiddlewares({ - routes: [ - { - matcher: "/store/carts/:id/line-items-metals", - method: "POST", - middlewares: [ - validateAndTransformBody( - StoreAddCartLineItem - ), - ], - }, - ], -}) -``` - -In this file, you export the middlewares definition using `defineMiddlewares` from the Medusa Framework. This function accepts an object having a `routes` property, which is an array of middleware configurations to apply on routes. - -You pass in the `routes` array an object having the following properties: - -- `matcher`: The route to apply the middleware on. -- `method`: The HTTP method to apply the middleware on for the specified API route. -- `middlewares`: An array of the middlewares to apply. You apply the `validateAndTransformBody` middleware, which validates the request body based on the `StoreAddCartLineItem` schema. This validation schema is the same schema used for Medusa's [Add Item to Cart API Route](https://docs.medusajs.com/api/store#carts_postcartsidlineitems). - -Any request sent to the `/store/carts/:id/line-items-metals` API route will now fail if it doesn't have the required parameters. - -Learn more about API route validation in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/validation/index.html.md). - -### Prepare to Test API Route - -Before you test the API route, you'll prepare and retrieve the necessary data to add a product variant with a custom price to the cart. - -#### Create Product with Metal Variant - -You'll first create a product that has a `Metal` option, and variant(s) with values for this option. - -Start the Medusa application with the following command: - -```bash npm2yarn -npm run dev -``` - -Then, open the Medusa Admin dashboard at `localhost:9000/app` and log in with the email and password you created when you installed the Medusa application in the first step. - -Once you log in, click on Products in the sidebar, then click the Create button at the top right. - -![Click on Products in the sidebar at the left, then click on the Create button at the top right of the content](https://res.cloudinary.com/dza7lstvk/image/upload/v1738253415/Medusa%20Resources/Screenshot_2025-01-30_at_6.09.36_PM_ee0jr2.png) - -Then, in the Create Product form: - -1. Enter a name for the product, and optionally enter other details like description. -2. Enable the "Yes, this is a product with variants" toggle. -3. Under Product Options, enter "Metal" for the title, and enter "Gold" for the values. - -Once you're done, click the Continue button. - -![Fill in the product details, enable the "Yes, this is a product with variants" toggle, and add the "Metal" option with "Gold" value](https://res.cloudinary.com/dza7lstvk/image/upload/v1738253520/Medusa%20Resources/Screenshot_2025-01-30_at_6.11.29_PM_lqxth9.png) - -You can skip the next two steps by clicking the Continue button again, then the Publish button. - -Once you're done, the product's page will open. You'll now add weight to the product's Gold variant. To do that: - -- Scroll to the Variants section and find the Gold variant. -- Click on the three-dots icon at its right. -- Choose "Edit" from the dropdown. - -![Find the Gold variant in the Variants section, click on the three-dots icon, and choose "Edit"](https://res.cloudinary.com/dza7lstvk/image/upload/v1738254038/Medusa%20Resources/Screenshot_2025-01-30_at_6.19.52_PM_j3hjcx.png) - -In the side window that opens, find the Weight field, enter the weight, and click the Save button. - -![Enter the weight in the Weight field, then click the Save button](https://res.cloudinary.com/dza7lstvk/image/upload/v1738254165/Medusa%20Resources/Screenshot_2025-01-30_at_6.22.15_PM_yplzdp.png) - -Finally, you need to set fixed prices for the variant, even if they're just `0`. To do that: - -1. Click on the three-dots icon at the top right of the Variants section. -2. Choose "Edit Prices" from the dropdown. - -![Click on the three-dots icon at the top right of the Variants section, then choose "Edit Prices"](https://res.cloudinary.com/dza7lstvk/image/upload/v1738255203/Medusa%20Resources/Screenshot_2025-01-30_at_6.39.35_PM_s3jpxh.png) - -For each cell in the table, either enter a fixed price for the specified currency or leave it as `0`. Once you're done, click the Save button. - -![Enter fixed prices for the variant in the table, then click the Save button](https://res.cloudinary.com/dza7lstvk/image/upload/v1738255272/Medusa%20Resources/Screenshot_2025-01-30_at_6.40.45_PM_zw1l59.png) - -You'll use this variant to add it to the cart later. You can find its ID by clicking on the variant, opening its details page. Then, on the details page, click on the icon at the right of the JSON section, and copy the ID from the JSON data. - -![Click on the icon at the right of the JSON section to copy the variant's ID](https://res.cloudinary.com/dza7lstvk/image/upload/v1738254314/Medusa%20Resources/Screenshot_2025-01-30_at_6.24.49_PM_ka7xew.png) - -#### Retrieve Publishable API Key - -All requests sent to API routes starting with `/store` must have a publishable API key in the header. This ensures the request's operations are scoped to the publishable API key's associated sales channels. For example, products that aren't available in a cart's sales channel can't be added to it. - -To retrieve the publishable API key, on the Medusa Admin: - -1. Click on Settings in the sidebar at the bottom left. -2. Click on Publishable API Keys from the sidebar, then click on a publishable API key in the list. - -![Click on publishable API keys in the Settings sidebar, then click on a publishable API key in the list](https://res.cloudinary.com/dza7lstvk/image/upload/v1738254523/Medusa%20Resources/Screenshot_2025-01-30_at_6.28.17_PM_mldscc.png) - -3. Click on the publishable API key to copy it. - -![Click on the publishable API key to copy it](https://res.cloudinary.com/dza7lstvk/image/upload/v1738254601/Medusa%20Resources/Screenshot_2025-01-30_at_6.29.26_PM_vvatki.png) - -You'll use this key when you test the API route. - -### Test API Route - -To test out the API route, you need to create a cart. A cart must be associated with a region. So, to retrieve the ID of a region in your store, send a `GET` request to the `/store/regions` API route: - -```bash -curl 'localhost:9000/store/regions' \ --H 'x-publishable-api-key: {api_key}' -``` - -Make sure to replace `{api_key}` with the publishable API key you copied earlier. - -This will return a list of regions. Copy the ID of one of the regions. - -Then, send a `POST` request to the `/store/carts` API route to create a cart: - -```bash -curl -X POST 'localhost:9000/store/carts' \ --H 'x-publishable-api-key: {api_key}' \ --H 'Content-Type: application/json' \ ---data '{ - "region_id": "{region_id}" -}' -``` - -Make sure to replace `{api_key}` with the publishable API key you copied earlier, and `{region_id}` with the ID of a region from the previous request. - -This will return the created cart. Copy the ID of the cart to use it next. - -Finally, to add the Gold variant to the cart with a custom price, send a `POST` request to the `/store/carts/:id/line-items-metals` API route: - -```bash -curl -X POST 'localhost:9000/store/carts/{cart_id}/line-items-metals' \ --H 'x-publishable-api-key: {api_key}' \ --H 'Content-Type: application/json' \ ---data '{ - "variant_id": "{variant_id}", - "quantity": 1 -}' -``` - -Make sure to replace: - -- `{api_key}` with the publishable API key you copied earlier. -- `{cart_id}` with the ID of the cart you created. -- `{variant_id}` with the ID of the Gold variant you created. - -This will return the cart's details, where you can see in its `items` array the item with the custom price: - -```json title="Example Response" -{ - "cart": { - "items": [ - { - "variant_id": "{variant_id}", - "quantity": 1, - "is_custom_price": true, - // example custom price - "unit_price": 2000 - } - ] - } -} -``` - -The price will be the result of the calculation you've implemented earlier, which is the fixed price of the variant plus the real-time price of the metal, multiplied by the weight of the variant and the quantity added to the cart. - -This price will be reflected in the cart's total price, and you can proceed to checkout with the custom-priced item. - -*** - -## Next Steps - -You've now implemented custom item pricing in Medusa. You can also customize the [storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) to use the new API route to add custom-priced items to the cart. - -If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth learning of all the concepts you've used in this guide and more. - -To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md). - - # How-to & Tutorials In this section of the documentation, you'll find how-to guides and tutorials that will help you customize the Medusa server and admin. These guides are useful after you've learned Medusa's main concepts in the [Get Started](https://docs.medusajs.com/docs/learn/index.html.md) section of the documentation. @@ -39862,6 +39990,2428 @@ For a quick access to code snippets of the different concepts you learned about, Deployment guides are a collection of guides that help you deploy your Medusa server and admin to different platforms. Learn more in the [Deployment Overview](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/deployment/index.html.md) documentation. +# Send Abandoned Cart Notifications in Medusa + +In this tutorial, you will learn how to send notifications to customers who have abandoned their carts. + +When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md), which are available out-of-the-box. These features include cart-management capabilities. + +Medusa's [Notification Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/index.html.md) allows you to send notifications to users or customers, such as password reset emails, order confirmation SMS, or other types of notifications. + +In this tutorial, you will use the Notification Module to send an email to customers who have abandoned their carts. The email will contain a link to recover the customer's cart, encouraging them to complete their purchase. You will use SendGrid to send the emails, but you can also use other email providers. + +## Summary + +By following this tutorial, you will: + +- Install and set up Medusa. +- Create the logic to send an email to customers who have abandoned their carts. +- Run the above logic once a day. +- Add a route to the storefront to recover the cart. + +![Diagram illustrating the flow of the abandoned-cart functionalities](https://res.cloudinary.com/dza7lstvk/image/upload/v1742460588/Medusa%20Resources/abandoned-cart-summary_fcf2tn.jpg) + +[View on Github](https://github.com/medusajs/examples/tree/main/abandoned-cart): Find the full code for this tutorial. + +*** + +## Step 1: Install a Medusa Application + +### Prerequisites + +- [Node.js v20+](https://nodejs.org/en/download) +- [Git CLI tool](https://git-scm.com/downloads) +- [PostgreSQL](https://www.postgresql.org/download/) + +Start by installing the Medusa application on your machine with the following command: + +```bash +npx create-medusa-app@latest +``` + +You will first be asked for the project's name. Then, when asked whether you want to install the [Next.js starter storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose "Yes." + +Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name and the Next.js Starter Storefront in a separate directory with the `{project-name}-storefront` name. + +The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md). + +Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterwards, you can log in with the new user and explore the dashboard. + +Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help. + +*** + +## Step 2: Set up SendGrid + +### Prerequisites + +- [SendGrid account](https://sendgrid.com) +- [Verified Sender Identity](https://mc.sendgrid.com/senders) +- [SendGrid API Key](https://app.sendgrid.com/settings/api_keys) + +Medusa's Notification Module provides the general functionality to send notifications, but the sending logic is implemented in a module provider. This allows you to integrate the email provider of your choice. + +To send the cart-abandonment emails, you will use SendGrid. Medusa provides a [SendGrid Notification Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/sendgrid/index.html.md) that you can use to send emails. + +Alternatively, you can use [other Notification Module Providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification#what-is-a-notification-module-provider/index.html.md) or [create a custom provider](https://docs.medusajs.com/references/notification-provider-module/index.html.md). + +To set up SendGrid, add the SendGrid Notification Module Provider to `medusa-config.ts`: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/notification", + options: { + providers: [ + { + resolve: "@medusajs/medusa/notification-sendgrid", + id: "sendgrid", + options: { + channels: ["email"], + api_key: process.env.SENDGRID_API_KEY, + from: process.env.SENDGRID_FROM, + }, + }, + ], + }, + }, + ], +}) +``` + +In the `modules` configuration, you pass the Notification Provider and add SendGrid as a provider. You also pass to the SendGrid Module Provider the following options: + +- `channels`: The channels that the provider supports. In this case, it is only email. +- `api_key`: Your SendGrid API key. +- `from`: The email address that the emails will be sent from. + +Then, set the SendGrid API key and "from" email as environment variables, such as in the `.env` file at the root of your project: + +```plain +SENDGRID_API_KEY=your-sendgrid-api-key +SENDGRID_FROM=test@gmail.com +``` + +You can now use SendGrid to send emails in Medusa. + +*** + +## Step 3: Send Abandoned Cart Notification Flow + +You will now implement the sending logic for the abandoned cart notifications. + +To build custom commerce features in Medusa, you create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow like you construct a function, but it is a special function that allows you to track its executions' progress, define roll-back logic, and configure other advanced features. Then, you execute the workflow from other customizations, such as in a scheduled job. + +In this step, you will create the workflow that sends the abandoned cart notifications. Later, you will learn how to execute it once a day. + +The workflow will receive the list of abandoned carts as an input. The workflow has the following steps: + +- [sendAbandonedNotificationsStep](#sendAbandonedNotificationsStep): Send the abandoned cart notifications. +- [updateCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartsStep/index.html.md): Update the cart to store the last notification date. + +Medusa provides the second step in its `@medusajs/medusa/core-flows` package. So, you only need to implement the first one. + +### sendAbandonedNotificationsStep + +The first step of the workflow sends a notification to the owners of the abandoned carts that are passed as an input. + +To implement the step, create the file `src/workflows/steps/send-abandoned-notifications.ts` with the following content: + +```ts title="src/workflows/steps/send-abandoned-notifications.ts" +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" +import { CartDTO, CustomerDTO } from "@medusajs/framework/types" + +type SendAbandonedNotificationsStepInput = { + carts: (CartDTO & { + customer: CustomerDTO + })[] +} + +export const sendAbandonedNotificationsStep = createStep( + "send-abandoned-notifications", + async (input: SendAbandonedNotificationsStepInput, { container }) => { + const notificationModuleService = container.resolve( + Modules.NOTIFICATION + ) + + const notificationData = input.carts.map((cart) => ({ + to: cart.email!, + channel: "email", + template: process.env.ABANDONED_CART_TEMPLATE_ID || "", + data: { + customer: { + first_name: cart.customer?.first_name || cart.shipping_address?.first_name, + last_name: cart.customer?.last_name || cart.shipping_address?.last_name, + }, + cart_id: cart.id, + items: cart.items?.map((item) => ({ + product_title: item.title, + quantity: item.quantity, + unit_price: item.unit_price, + thumbnail: item.thumbnail, + })), + }, + })) + + const notifications = await notificationModuleService.createNotifications( + notificationData + ) + + return new StepResponse({ + notifications, + }) + } +) +``` + +You create a step with `createStep` from the Workflows SDK. It accepts two parameters: + +1. The step's unique name, which is `create-review`. +2. An async function that receives two parameters: + - The step's input, which is in this case an object with the review's properties. + - An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step. + +In the step function, you first resolve the Notification Module's service, which has methods to manage notifications. Then, you prepare the data of each notification, and create the notifications with the `createNotifications` method. + +Notice that each notification is an object with the following properties: + +- `to`: The email address of the customer. +- `channel`: The channel that the notification will be sent through. The Notification Module uses the provider registered for the channel. +- `template`: The ID or name of the email template in the third-party provider. Make sure to set it as an environment variable once you have it. +- `data`: The data to pass to the template to render the email's dynamic content. + +Based on the dynamic template you create in SendGrid or another provider, you can pass different data in the `data` object. + +A step function must return a `StepResponse` instance. The `StepResponse` constructor accepts the step's output as a parameter, which is the created notifications. + +### Create Workflow + +You can now create the workflow that uses the step you just created to send the abandoned cart notifications. + +Create the file `src/workflows/send-abandoned-carts.ts` with the following content: + +```ts title="src/workflows/send-abandoned-carts.ts" +import { + createWorkflow, + WorkflowResponse, + transform, +} from "@medusajs/framework/workflows-sdk" +import { + sendAbandonedNotificationsStep, +} from "./steps/send-abandoned-notifications" +import { updateCartsStep } from "@medusajs/medusa/core-flows" +import { CartDTO } from "@medusajs/framework/types" +import { CustomerDTO } from "@medusajs/framework/types" + +export type SendAbandonedCartsWorkflowInput = { + carts: (CartDTO & { + customer: CustomerDTO + })[] +} + +export const sendAbandonedCartsWorkflow = createWorkflow( + "send-abandoned-carts", + function (input: SendAbandonedCartsWorkflowInput) { + sendAbandonedNotificationsStep(input) + + const updateCartsData = transform( + input, + (data) => { + return data.carts.map((cart) => ({ + id: cart.id, + metadata: { + ...cart.metadata, + abandoned_notification: new Date().toISOString(), + }, + })) + } + ) + + const updatedCarts = updateCartsStep(updateCartsData) + + return new WorkflowResponse(updatedCarts) + } +) +``` + +You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter. + +It accepts as a second parameter a constructor function, which is the workflow's implementation. The function can accept input, which in this case is an arra of carts. + +In the workflow's constructor function, you: + +- Use the `sendAbandonedNotificationsStep` to send the notifications to the carts' customers. +- Use the `updateCartsStep` from Medusa's core flows to update the carts' metadata with the last notification date. + +Notice that you use the `transform` function to prepare the `updateCartsStep`'s input. Medusa does not support direct data manipulation in a workflow's constructor function. You can learn more about it in the [Data Manipulation in Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md). + +Your workflow is now ready for use. You will learn how to execute it in the next section. + +### Setup Email Template + +Before you can test the workflow, you need to set up an email template in SendGrid. The template should contain the dynamic content that you pass in the workflow's step. + +To create an email template in SendGrid: + +- Go to [Dynamic Templates](https://mc.sendgrid.com/dynamic-templates) in the SendGrid dashboard. +- Click on the "Create Dynamic Template" button. + +![Button is at the top right of the page](https://res.cloudinary.com/dza7lstvk/image/upload/v1742457153/Medusa%20Resources/Screenshot_2025-03-20_at_9.51.38_AM_g5nk80.png) + +- In the side window that opens, enter a name for the template, then click on the Create button. +- The template will be added to the middle of the page. When you click on it, a new section will show with an "Add Version" button. Click on it. + +![The template is a collapsible in the middle of the page,with the "Add Version" button shown in the middle](https://res.cloudinary.com/dza7lstvk/image/upload/v1742458096/Medusa%20Resources/Screenshot_2025-03-20_at_10.07.54_AM_y2dys7.png) + +In the form that opens, you can either choose to start with a blank template or from an existing design. You can then use the drag-and-drop or code editor to design the email template. + +You can also use the following template as an example: + +```html title="Abandoned Cart Email Template" + + + + + + Complete Your Purchase + + + +
+
Hi {{customer.first_name}}, your cart is waiting! 🛍️
+

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

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

{{subtitle}}

+

Quantity: {{quantity}}

+

Price: $ {{unit_price}}

+
+
+ {{/each}} + + Return to Cart & Checkout + +
+ + +``` + +This template will show each item's image, title, quantity, and price in the cart. It will also show a button to return to the cart and checkout. + +You can replace `https://yourstore.com` with your storefront's URL. You'll later implement the `/cart/recover/:cart_id` route in the storefront to recover the cart. + +Once you are done, copy the template ID from SendGrid and set it as an environment variable in your Medusa project: + +```plain +ABANDONED_CART_TEMPLATE_ID=your-sendgrid-template-id +``` + +*** + +## Step 4: Schedule Cart Abandonment Notifications + +The next step is to automate sending the abandoned cart notifications. You need a task that runs once a day to find the carts that have been abandoned for a certain period and send the notifications to the customers. + +To run a task at a scheduled interval, you can use a [scheduled job](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md). A scheduled job is an asynchronous function that the Medusa application runs at the interval you specify during the Medusa application's runtime. + +You can create a scheduled job in a TypeScript or JavaScript file under the `src/jobs` directory. So, to create the scheduled job that sends the abandoned cart notifications, create the file `src/jobs/send-abandoned-cart-notification.ts` with the following content: + +```ts title="src/jobs/send-abandoned-cart-notification.ts" +import { MedusaContainer } from "@medusajs/framework/types" +import { + sendAbandonedCartsWorkflow, + SendAbandonedCartsWorkflowInput, +} from "../workflows/send-abandoned-carts" + +export default async function abandonedCartJob( + container: MedusaContainer +) { + const logger = container.resolve("logger") + const query = container.resolve("query") + + const oneDayAgo = new Date() + oneDayAgo.setDate(oneDayAgo.getDate() - 1) + const limit = 100 + const offset = 0 + const totalCount = 0 + const abandonedCartsCount = 0 + + do { + // TODO retrieve paginated abandoned carts + } while (offset < totalCount) + + logger.info(`Sent ${abandonedCartsCount} abandoned cart notifications`) +} + +export const config = { + name: "abandoned-cart-notification", + schedule: "0 0 * * *", // Run at midnight every day +} +``` + +In a scheduled job's file, you must export: + +1. An asynchronous function that holds the job's logic. The function receives the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) as a parameter. +2. A `config` object that specifies the job's name and schedule. The schedule is a [cron expression](https://crontab.guru/) that defines the interval at which the job runs. + +In the scheduled job function, so far you resolve the [Logger](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md) to log messages, and [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to retrieve data across modules. + +You also define a `oneDayAgo` date, which is the date that you will use as the condition of an abandoned cart. In addition, you define variables to paginate the carts. + +Next, you will retrieve the abandoned carts using Query. Replace the `TODO` with the following: + +```ts title="src/jobs/send-abandoned-cart-notification.ts" +const { + data: abandonedCarts, + metadata, +} = await query.graph({ + entity: "cart", + fields: [ + "id", + "email", + "items.*", + "metadata", + "customer.*", + ], + filters: { + updated_at: { + $lt: oneDayAgo, + }, + // @ts-ignore + email: { + $ne: null, + }, + // @ts-ignore + completed_at: null, + }, + pagination: { + skip: offset, + take: limit, + }, +}) + +totalCount = metadata?.count ?? 0 +const cartsWithItems = abandonedCarts.filter((cart) => + cart.items?.length > 0 && !cart.metadata?.abandoned_notification +) + +try { + await sendAbandonedCartsWorkflow(container).run({ + input: { + carts: cartsWithItems, + } as unknown as SendAbandonedCartsWorkflowInput, + }) + abandonedCartsCount += cartsWithItems.length + +} catch (error) { + logger.error( + `Failed to send abandoned cart notification: ${error.message}` + ) +} + +offset += limit +``` + +In the do-while loop, you use Query to retrieve carts matching the following criteria: + +- The cart was last updated more than a day ago. +- The cart has an email address. +- The cart has not been completed. + +You also filter the retrieved carts to only include carts with items and customers that have not received an abandoned cart notification. + +Finally, you execute the `sendAbandonedCartsWorkflow` passing it the abandoned carts as an input. You will execute the workflow for each paginated batch of carts. + +### Test it Out + +To test out the scheduled job and workflow, it is recommended to change the `oneDayAgo` date to a minute before now for easy testing: + +```ts title="src/jobs/send-abandoned-cart-notification.ts" +oneDayAgo.setMinutes(oneDayAgo.getMinutes() - 1) // For testing +``` + +And to change the job's schedule in `config` to run every minute: + +```ts title="src/jobs/send-abandoned-cart-notification.ts" +export const config = { + // ... + schedule: "* * * * *", // Run every minute for testing +} +``` + +Finally, start the Medusa application with the following command: + +```bash npm2yarn +npm run dev +``` + +And in the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md)'s directory (that you installed in the first step), start the storefront with the following command: + +```bash npm2yarn +npm run dev +``` + +Open the storefront at `localhost:8000`. You can either: + +- Create an account and add items to the cart, then leave the cart for a minute. +- Add an item to the cart as a guest. Then, start the checkout process, but only enter the shipping and email addresses, and leave the cart for a minute. + +Afterwards, wait for the job to execute. Once it is executed, you will see the following message in the terminal: + +```bash +info: Sent 1 abandoned cart notifications +``` + +Once you're done testing, make sure to revert the changes to the `oneDayAgo` date and the job's schedule. + +*** + +## Step 5: Recover Cart in Storefront + +In the storefront, you need to add a route that recovers the cart when the customer clicks on the link in the email. The route should receive the cart ID, set the cart ID in the cookie, and redirect the customer to the cart page. + +To implement the route, in the Next.js Starter Storefront create the file `src/app/[countryCode]/(main)/cart/recover/[id]/route.tsx` with the following content: + +```tsx title="src/app/[countryCode]/(main)/cart/recover/[id]/route.tsx" badgeLabel="Storefront" badgeColor="blue" +import { NextRequest } from "next/server" +import { retrieveCart } from "../../../../../../lib/data/cart" +import { setCartId } from "../../../../../../lib/data/cookies" +import { notFound, redirect } from "next/navigation" +type Params = Promise<{ + id: string +}> + +export async function GET(req: NextRequest, { params }: { params: Params }) { + const { id } = await params + const cart = await retrieveCart(id) + + if (!cart) { + return notFound() + } + + setCartId(id) + + const countryCode = cart.shipping_address?.country_code || + cart.region?.countries?.[0]?.iso_2 + + redirect( + `/${countryCode ? `${countryCode}/` : ""}cart` + ) +} +``` + +You add a `GET` route handler that receives the cart ID as a path parameter. In the route handler, you: + +- Try to retrieve the cart from the Medusa application. The `retrieveCart` function is already available in the Next.js 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 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 title="Medusa Application" +npm run dev +``` + +Then, run the following command in the Next.js Starter Storefront's directory to start the Next.js server: + +```bash title="Next.js Starter Storefront" +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/Users/shahednasser/medusa/www/apps/resources/app/events-reference/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). + + # Implement Product Reviews in Medusa In this tutorial, you'll learn how to implement product reviews in Medusa. @@ -41732,2428 +44282,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). -# 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 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 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 title="Medusa Application" -npm run dev -``` - -Then, run the following command in the Next.js Starter Storefront's directory to start the Next.js server: - -```bash title="Next.js Starter Storefront" -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/Users/shahednasser/medusa/www/apps/resources/app/events-reference/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. @@ -44239,1054 +44367,6 @@ Integrate a search engine to index and search products or other types of data in - [Algolia](https://docs.medusajs.com/integrations/guides/algolia/index.html.md) -# 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 Algolia (Search) with Medusa In this tutorial, you'll learn how to integrate Medusa with Algolia. @@ -46501,6 +45581,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). + + # Integrate Medusa with Resend (Email Notifications) In this guide, you'll learn how to integrate Medusa with Resend. @@ -52142,332 +52270,332 @@ To learn more about the commerce features that Medusa provides, check out Medusa ## JS SDK Admin -- [create](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.create/index.html.md) -- [batchPromotions](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.batchPromotions/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.delete/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.list/index.html.md) -- [addInboundItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addInboundItems/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.retrieve/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) -- [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) -- [addOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addOutboundShipping/index.html.md) -- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.cancelRequest/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.create/index.html.md) -- [deleteInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.deleteInboundShipping/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.update/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) -- [removeItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeItem/index.html.md) -- [request](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.request/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.retrieve/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) -- [updateOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateOutboundItem/index.html.md) -- [updateItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateItem/index.html.md) -- [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) - [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) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.retrieve/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.list/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.delete/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) -- [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) - [update](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.update/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) - [clearToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.clearToken_/index.html.md) -- [fetchStream](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.fetchStream/index.html.md) - [getApiKeyHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getApiKeyHeader_/index.html.md) -- [getTokenStorageInfo\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getTokenStorageInfo_/index.html.md) -- [getToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getToken_/index.html.md) -- [getPublishableKeyHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getPublishableKeyHeader_/index.html.md) - [getJwtHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getJwtHeader_/index.html.md) +- [fetchStream](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.fetchStream/index.html.md) +- [getToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getToken_/index.html.md) +- [getTokenStorageInfo\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getTokenStorageInfo_/index.html.md) +- [getPublishableKeyHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getPublishableKeyHeader_/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) -- [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) -- [batchCustomers](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.batchCustomers/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.delete/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.list/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.create/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.retrieve/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.update/index.html.md) -- [getItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.getItem/index.html.md) -- [batchCustomerGroups](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.batchCustomerGroups/index.html.md) -- [setItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.setItem/index.html.md) +- [batchPromotions](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.batchPromotions/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.delete/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.create/index.html.md) +- [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) +- [addInboundItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addInboundItems/index.html.md) +- [addInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addInboundShipping/index.html.md) +- [addItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addItems/index.html.md) +- [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) +- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.cancelRequest/index.html.md) +- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.cancel/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.create/index.html.md) +- [deleteInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.deleteInboundShipping/index.html.md) +- [deleteOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.deleteOutboundShipping/index.html.md) +- [removeInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeInboundItem/index.html.md) +- [removeItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeItem/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.list/index.html.md) +- [removeOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeOutboundItem/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.retrieve/index.html.md) +- [updateInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateInboundItem/index.html.md) +- [request](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.request/index.html.md) +- [updateInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateInboundShipping/index.html.md) +- [updateItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateItem/index.html.md) +- [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) - [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) +- [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) +- [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) -- [deleteAddress](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.deleteAddress/index.html.md) -- [listAddresses](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.listAddresses/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.list/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) +- [deleteAddress](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.deleteAddress/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.retrieve/index.html.md) +- [listAddresses](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.listAddresses/index.html.md) +- [retrieveAddress](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.retrieveAddress/index.html.md) - [updateAddress](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.updateAddress/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.update/index.html.md) -- [retrieveAddress](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.retrieveAddress/index.html.md) +- [batchCustomers](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.batchCustomers/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.list/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.delete/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.retrieve/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.create/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.update/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) -- [beginEdit](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.beginEdit/index.html.md) -- [cancelEdit](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.cancelEdit/index.html.md) - [addShippingMethod](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.addShippingMethod/index.html.md) +- [beginEdit](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.beginEdit/index.html.md) +- [addPromotions](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.addPromotions/index.html.md) +- [cancelEdit](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.cancelEdit/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) - [create](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.create/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.list/index.html.md) - [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) -- [removeActionItem](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.removeActionItem/index.html.md) - [removeActionShippingMethod](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.removeActionShippingMethod/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.retrieve/index.html.md) -- [updateActionShippingMethod](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.updateActionShippingMethod/index.html.md) +- [removeActionItem](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.removeActionItem/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.update/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.retrieve/index.html.md) - [updateActionItem](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.updateActionItem/index.html.md) -- [updateItem](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.updateItem/index.html.md) +- [updateActionShippingMethod](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.updateActionShippingMethod/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) +- [updateItem](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.updateItem/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) +- [addInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addInboundShipping/index.html.md) - [addOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addOutboundShipping/index.html.md) - [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.cancelRequest/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.create/index.html.md) +- [deleteInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.deleteInboundShipping/index.html.md) - [cancel](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.cancel/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) - [removeInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.removeInboundItem/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.list/index.html.md) - [removeOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.removeOutboundItem/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.retrieve/index.html.md) -- [updateInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateInboundShipping/index.html.md) - [request](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.request/index.html.md) -- [updateInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateInboundItem/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.retrieve/index.html.md) - [updateOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateOutboundItem/index.html.md) +- [updateInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateInboundShipping/index.html.md) +- [updateInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateInboundItem/index.html.md) - [updateOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateOutboundShipping/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.create/index.html.md) - [createShipment](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.createShipment/index.html.md) -- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.cancel/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.delete/index.html.md) -- [deleteServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.deleteServiceZone/index.html.md) - [createServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.createServiceZone/index.html.md) -- [retrieveServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.retrieveServiceZone/index.html.md) +- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.cancel/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) -- [accept](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.accept/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.create/index.html.md) -- [resend](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.resend/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.delete/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.list/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentProvider/methods/js_sdk.admin.FulfillmentProvider.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.retrieve/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) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.delete/index.html.md) +- [retrieveServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.retrieveServiceZone/index.html.md) - [batchUpdateLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.batchUpdateLevels/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) - [delete](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.delete/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.create/index.html.md) -- [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) +- [deleteLevel](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.deleteLevel/index.html.md) - [listLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.listLevels/index.html.md) - [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) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Notification/methods/js_sdk.admin.Notification.list/index.html.md) - [updateLevel](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.updateLevel/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentProvider/methods/js_sdk.admin.FulfillmentProvider.list/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Notification/methods/js_sdk.admin.Notification.list/index.html.md) +- [listFulfillmentOptions](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentProvider/methods/js_sdk.admin.FulfillmentProvider.listFulfillmentOptions/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) +- [accept](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.accept/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.create/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.list/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.delete/index.html.md) +- [resend](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.resend/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.retrieve/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) - [cancelTransfer](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancelTransfer/index.html.md) - [createCreditLine](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.createCreditLine/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) -- [createShipment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.createShipment/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.list/index.html.md) -- [listLineItems](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.listLineItems/index.html.md) +- [createShipment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.createShipment/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) -- [retrievePreview](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.retrievePreview/index.html.md) - [markAsDelivered](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.markAsDelivered/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.retrieve/index.html.md) +- [listLineItems](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.listLineItems/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.update/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.retrieve/index.html.md) +- [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) - [removeAddedItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.removeAddedItem/index.html.md) - [request](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.request/index.html.md) -- [updateAddedItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.updateAddedItem/index.html.md) - [updateOriginalItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.updateOriginalItem/index.html.md) +- [updateAddedItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.updateAddedItem/index.html.md) - [capture](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.capture/index.html.md) - [listPaymentProviders](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.listPaymentProviders/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.list/index.html.md) - [refund](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.refund/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.list/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.retrieve/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/PaymentCollection/methods/js_sdk.admin.PaymentCollection.create/index.html.md) - [markAsPaid](https://docs.medusajs.com/references/js_sdk/admin/PaymentCollection/methods/js_sdk.admin.PaymentCollection.markAsPaid/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Plugin/methods/js_sdk.admin.Plugin.list/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/PaymentCollection/methods/js_sdk.admin.PaymentCollection.delete/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) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.retrieve/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.list/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/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) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.retrieve/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.update/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.create/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.delete/index.html.md) - [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) -- [update](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.update/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.retrieve/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.delete/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.list/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.create/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.update/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.delete/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.list/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.retrieve/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.update/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.update/index.html.md) - [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.updateProducts/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.create/index.html.md) +- [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) +- [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) +- [createOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createOption/index.html.md) +- [batchVariants](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batchVariants/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) +- [deleteOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.deleteOption/index.html.md) +- [deleteVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.deleteVariant/index.html.md) +- [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) +- [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) +- [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) - [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.delete/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.create/index.html.md) +- [retrieveVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieveVariant/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.list/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.retrieve/index.html.md) - [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) -- [batchVariantInventoryItems](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batchVariantInventoryItems/index.html.md) -- [confirmImport](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.confirmImport/index.html.md) -- [batch](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batch/index.html.md) -- [batchVariants](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batchVariants/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.create/index.html.md) -- [createOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createOption/index.html.md) -- [createVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createVariant/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.delete/index.html.md) -- [deleteOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.deleteOption/index.html.md) -- [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) -- [listVariants](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.listVariants/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.list/index.html.md) -- [listOptions](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.listOptions/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieve/index.html.md) -- [retrieveVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieveVariant/index.html.md) -- [retrieveOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieveOption/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.update/index.html.md) -- [updateOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.updateOption/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.create/index.html.md) -- [updateVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.updateVariant/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/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) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.retrieve/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.list/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.update/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/ProductVariant/methods/js_sdk.admin.ProductVariant.list/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.update/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.create/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.delete/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.list/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.retrieve/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.list/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.update/index.html.md) -- [addRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.addRules/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/RefundReason/methods/js_sdk.admin.RefundReason.list/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.create/index.html.md) -- [listRuleAttributes](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRuleAttributes/index.html.md) +- [addRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.addRules/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.list/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.delete/index.html.md) +- [listRuleAttributes](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRuleAttributes/index.html.md) - [listRuleValues](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRuleValues/index.html.md) -- [listRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRules/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) - [create](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.create/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.list/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.delete/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.retrieve/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.update/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/RefundReason/methods/js_sdk.admin.RefundReason.list/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.delete/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.create/index.html.md) -- [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) -- [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/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) - [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) - [cancel](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancel/index.html.md) - [cancelReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancelReceive/index.html.md) - [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancelRequest/index.html.md) -- [confirmRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.confirmRequest/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) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.list/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) - [initiateRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.initiateRequest/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.list/index.html.md) - [receiveItems](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.receiveItems/index.html.md) - [removeDismissItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeDismissItem/index.html.md) - [removeReceiveItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeReceiveItem/index.html.md) - [removeReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeReturnItem/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.retrieve/index.html.md) - [updateDismissItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateDismissItem/index.html.md) -- [updateRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateRequest/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) -- [create](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.create/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/ReturnReason/methods/js_sdk.admin.ReturnReason.create/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.delete/index.html.md) -- [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) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.delete/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.create/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.list/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.update/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.retrieve/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.create/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.delete/index.html.md) - [batchProducts](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.batchProducts/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.delete/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/ShippingOption/methods/js_sdk.admin.ShippingOption.create/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.delete/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.list/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.retrieve/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.list/index.html.md) - [updateRules](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.updateRules/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.update/index.html.md) +- [createFulfillmentSet](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.createFulfillmentSet/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.create/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.list/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.delete/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.retrieve/index.html.md) +- [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) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.delete/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.create/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.retrieve/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.list/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.update/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/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/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) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.retrieve/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.list/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.update/index.html.md) -- [createFulfillmentSet](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.createFulfillmentSet/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.create/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.delete/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.retrieve/index.html.md) -- [updateFulfillmentProviders](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.updateFulfillmentProviders/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.update/index.html.md) -- [updateSalesChannels](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.updateSalesChannels/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Store/methods/js_sdk.admin.Store.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Store/methods/js_sdk.admin.Store.retrieve/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/Store/methods/js_sdk.admin.Store.update/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.create/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.delete/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.list/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.update/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.retrieve/index.html.md) -- [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/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) +- [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/User/methods/js_sdk.admin.User.delete/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.list/index.html.md) +- [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) - [list](https://docs.medusajs.com/references/js_sdk/admin/WorkflowExecution/methods/js_sdk.admin.WorkflowExecution.list/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/WorkflowExecution/methods/js_sdk.admin.WorkflowExecution.retrieve/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.delete/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.retrieve/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.list/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.update/index.html.md) -- [me](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.me/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.create/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.delete/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.retrieve/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.list/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.retrieve/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.create/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.delete/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.retrieve/index.html.md) ## 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) - [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) @@ -52478,10 +52606,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) - [cart](https://docs.medusajs.com/references/js-sdk/store/cart/index.html.md) -- [customer](https://docs.medusajs.com/references/js-sdk/store/customer/index.html.md) -- [fulfillment](https://docs.medusajs.com/references/js-sdk/store/fulfillment/index.html.md) -- [collection](https://docs.medusajs.com/references/js-sdk/store/collection/index.html.md) - [order](https://docs.medusajs.com/references/js-sdk/store/order/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) - [payment](https://docs.medusajs.com/references/js-sdk/store/payment/index.html.md) - [region](https://docs.medusajs.com/references/js-sdk/store/region/index.html.md) - [product](https://docs.medusajs.com/references/js-sdk/store/product/index.html.md) @@ -53291,490 +53419,287 @@ 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. -# Data Table - Admin Components +# Container - Admin Components -This component is available after [Medusa v2.4.0+](https://github.com/medusajs/medusa/releases/tag/v2.4.0). +The Medusa Admin wraps each section of a page in a container. -The [DataTable component in Medusa UI](https://docs.medusajs.com/ui/components/data-table/index.html.md) allows you to display data in a table with sorting, filtering, and pagination. It's used across the Medusa Admin dashboard to showcase a list of items, such as a list of products. +![Example of a container in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1728287102/Medusa%20Resources/container_soenir.png) -![Example of a table in the product listing page](https://res.cloudinary.com/dza7lstvk/image/upload/v1728295658/Medusa%20Resources/list_ddt9zc.png) +To create a component that uses the same container styling in your widgets or UI routes, create the file `src/admin/components/container.tsx` with the following content: -You can use this component in your Admin Extensions to display data in a table format, especially if you're retrieving them from API routes of the Medusa application. - -This guide focuses on how to use the `DataTable` component while fetching data from the backend. Refer to the [Medusa UI documentation](https://docs.medusajs.com/ui/components/data-table/index.html.md) for detailed information about the DataTable component and its different usages. - -## Example: DataTable with Data Fetching - -In this example, you'll create a UI widget that shows the list of products retrieved from the [List Products API Route](https://docs.medusajs.com/api/admin#products_getproducts) in a data table with pagination, filtering, searching, and sorting. - -Start by initializing the columns in the data table. To do that, use the `createDataTableColumnHelper` from Medusa UI: - -```tsx title="src/admin/routes/custom/page.tsx" -import { - createDataTableColumnHelper, -} from "@medusajs/ui" +```tsx import { - HttpTypes, -} from "@medusajs/framework/types" - -const columnHelper = createDataTableColumnHelper() - -const columns = [ - columnHelper.accessor("title", { - header: "Title", - // Enables sorting for the column. - enableSorting: true, - // If omitted, the header will be used instead if it's a string, - // otherwise the accessor key (id) will be used. - sortLabel: "Title", - // If omitted the default value will be "A-Z" - sortAscLabel: "A-Z", - // If omitted the default value will be "Z-A" - sortDescLabel: "Z-A", - }), - columnHelper.accessor("status", { - header: "Status", - cell: ({ getValue }) => { - const status = getValue() - return ( - - {status === "published" ? "Published" : "Draft"} - - ) - }, - }), -] -``` - -`createDataTableColumnHelper` utility creates a column helper that helps you define the columns for the data table. The column helper has an `accessor` method that accepts two parameters: - -1. The column's key in the table's data. -2. An object with the following properties: - - `header`: The column's header. - - `cell`: (optional) By default, a data's value for a column is displayed as a string. Use this property to specify custom rendering of the value. It accepts a function that returns a string or a React node. The function receives an object that has a `getValue` property function to retrieve the raw value of the cell. - - `enableSorting`: (optional) A boolean that enables sorting data by this column. - - `sortLabel`: (optional) The label for the sorting button. If omitted, the `header` will be used instead if it's a string, otherwise the accessor key (id) will be used. - - `sortAscLabel`: (optional) The label for the ascending sorting button. If omitted, the default value will be "A-Z". - - `sortDescLabel`: (optional) The label for the descending sorting button. If omitted, the default value will be "Z-A". - -Next, you'll define the filters that can be applied to the data table. You'll configure filtering by product status. - -To define the filters, add the following: - -```tsx title="src/admin/routes/custom/page.tsx" -// other imports... -import { - // ... - createDataTableFilterHelper, + Container as UiContainer, + clx, } from "@medusajs/ui" -const filterHelper = createDataTableFilterHelper() +type ContainerProps = React.ComponentProps -const filters = [ - filterHelper.accessor("status", { - type: "select", - label: "Status", - options: [ - { - label: "Published", - value: "published", - }, - { - label: "Draft", - value: "draft", - }, - ], - }), -] -``` - -`createDataTableFilterHelper` utility creates a filter helper that helps you define the filters for the data table. The filter helper has an `accessor` method that accepts two parameters: - -1. The key of a column in the table's data. -2. An object with the following properties: - - `type`: The type of filter. It can be either: - - `select`: A select dropdown allowing users to choose multiple values. - - `radio`: A radio button allowing users to choose one value. - - `date`: A date picker allowing users to choose a date. - - `label`: The filter's label. - - `options`: An array of objects with `label` and `value` properties. The `label` is the option's label, and the `value` is the value to filter by. - -You'll now start creating the UI widget's component. Start by adding the necessary state variables: - -```tsx title="src/admin/routes/custom/page.tsx" -// other imports... -import { - // ... - DataTablePaginationState, - DataTableFilteringState, - DataTableSortingState, -} from "@medusajs/ui" -import { useMemo, useState } from "react" - -// ... - -const limit = 15 - -const CustomPage = () => { - const [pagination, setPagination] = useState({ - pageSize: limit, - pageIndex: 0, - }) - const [search, setSearch] = useState("") - const [filtering, setFiltering] = useState({}) - const [sorting, setSorting] = useState(null) - - const offset = useMemo(() => { - return pagination.pageIndex * limit - }, [pagination]) - const statusFilters = useMemo(() => { - return (filtering.status || []) as ProductStatus - }, [filtering]) - - // TODO add data fetching logic +export const Container = (props: ContainerProps) => { + return ( + + ) } ``` -In the component, you've added the following state variables: +The `Container` component re-uses the component from the [Medusa UI package](https://docs.medusajs.com/ui/components/container/index.html.md) and applies to it classes to match the Medusa Admin's design conventions. -- `pagination`: An object of type `DataTablePaginationState` that holds the pagination state. It has two properties: - - `pageSize`: The number of items to show per page. - - `pageIndex`: The current page index. -- `search`: A string that holds the search query. -- `filtering`: An object of type `DataTableFilteringState` that holds the filtering state. -- `sorting`: An object of type `DataTableSortingState` that holds the sorting state. +*** -You've also added two memoized variables: +## Example -- `offset`: How many items to skip when fetching data based on the current page. -- `statusFilters`: The selected status filters, if any. +Use that `Container` component in any widget or UI route. -Next, you'll fetch the products from the Medusa application. Assuming you have the JS SDK configured as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md), add the following imports at the top of the file: +For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content: -```tsx title="src/admin/routes/custom/page.tsx" -import { sdk } from "../../lib/config" -import { useQuery } from "@tanstack/react-query" -``` - -This imports the JS SDK instance and `useQuery` from [Tanstack Query](https://tanstack.com/query/latest). - -Then, replace the `TODO` in the component with the following: - -```tsx title="src/admin/routes/custom/page.tsx" -const { data, isLoading } = useQuery({ - queryFn: () => sdk.admin.product.list({ - limit, - offset, - q: search, - status: statusFilters, - order: sorting ? `${sorting.desc ? "-" : ""}${sorting.id}` : undefined, - }), - queryKey: [["products", limit, offset, search, statusFilters, sorting?.id, sorting?.desc]], -}) - -// TODO configure data table -``` - -You use the `useQuery` hook to fetch the products from the Medusa application. In the `queryFn`, you call the `sdk.admin.product.list` method to fetch the products. You pass the following query parameters to the method: - -- `limit`: The number of products to fetch per page. -- `offset`: The number of products to skip based on the current page. -- `q`: The search query, if set. -- `status`: The status filters, if set. -- `order`: The sorting order, if set. - -So, whenever the user changes the current page, search query, status filters, or sorting, the products are fetched based on the new parameters. - -Next, you'll configure the data table. Medusa UI provides a `useDataTable` hook that helps you configure the data table. Add the following imports at the top of the file: - -```tsx title="src/admin/routes/custom/page.tsx" -import { - // ... - useDataTable, -} from "@medusajs/ui" -import { useNavigate } from "react-router-dom" -``` - -Then, replace the `TODO` in the component with the following: - -```tsx title="src/admin/routes/custom/page.tsx" -const navigate = useNavigate() - -const table = useDataTable({ - columns, - data: data?.products || [], - getRowId: (row) => row.id, - rowCount: data?.count || 0, - isLoading, - pagination: { - state: pagination, - onPaginationChange: setPagination, - }, - search: { - state: search, - onSearchChange: setSearch, - }, - filtering: { - state: filtering, - onFilteringChange: setFiltering, - }, - filters, - sorting: { - // Pass the pagination state and updater to the table instance - state: sorting, - onSortingChange: setSorting, - }, - onRowClick: (event, row) => { - // Handle row click, for example - navigate(`/products/${row.id}`) - }, -}) - -// TODO render component -``` - -The `useDataTable` hook accepts an object with the following properties: - -- columns: (\`array\`) The columns to display in the data table. You created this using the \`createDataTableColumnHelper\` utility. -- data: (\`array\`) The products fetched from the Medusa application. -- getRowId: (\`function\`) A function that returns the unique ID of a row. -- rowCount: (\`number\`) The total number of products that can be retrieved. This is used to determine the number of pages. -- isLoading: (\`boolean\`) A boolean that indicates if the data is being fetched. -- pagination: (\`object\`) An object to configure pagination. - - - state: (\`object\`) The pagination React state variable. - - - onPaginationChange: (\`function\`) A function that updates the pagination state. -- search: (\`object\`) An object to configure searching. - - - state: (\`string\`) The search query React state variable. - - - onSearchChange: (\`function\`) A function that updates the search query state. -- filtering: (\`object\`) An object to configure filtering. - - - state: (\`object\`) The filtering React state variable. - - - onFilteringChange: (\`function\`) A function that updates the filtering state. -- filters: (\`array\`) The filters to display in the data table. You created this using the \`createDataTableFilterHelper\` utility. -- sorting: (\`object\`) An object to configure sorting. - - - state: (\`object\`) The sorting React state variable. - - - onSortingChange: (\`function\`) A function that updates the sorting state. -- onRowClick: (\`function\`) A function that allows you to perform an action when the user clicks on a row. In this example, you navigate to the product's detail page. - - - event: (\`mouseevent\`) An instance of the \[MouseClickEvent]\(https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent) object. - - - row: (\`object\`) The data of the row that was clicked. - -Finally, you'll render the data table. But first, add the following imports at the top of the page: - -```tsx title="src/admin/routes/custom/page.tsx" -import { - // ... - DataTable, -} from "@medusajs/ui" -import { SingleColumnLayout } from "../../layouts/single-column" -import { Container } from "../../components/container" -``` - -Aside from the `DataTable` component, you also import the [SingleColumnLayout](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/layouts/single-column/index.html.md) and [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) components implemented in other Admin Component guides. These components ensure a style consistent to other pages in the admin dashboard. - -Then, replace the `TODO` in the component with the following: - -```tsx title="src/admin/routes/custom/page.tsx" -return ( - - - - - Products -
- - - -
-
- - -
-
-
-) -``` - -You render the `DataTable` component and pass the `table` instance as a prop. In the `DataTable` component, you render a toolbar showing a heading, filter menu, sorting menu, and a search input. You also show pagination after the table. - -Lastly, export the component and the UI widget's configuration at the end of the file: - -```tsx title="src/admin/routes/custom/page.tsx" -// other imports... -import { defineRouteConfig } from "@medusajs/admin-sdk" -import { ChatBubbleLeftRight } from "@medusajs/icons" - -// ... - -export const config = defineRouteConfig({ - label: "Custom", - icon: ChatBubbleLeftRight, -}) - -export default CustomPage -``` - -If you start your Medusa application and go to `localhost:9000/app/custom`, you'll see the data table showing the list of products with pagination, filtering, searching, and sorting functionalities. - -### Full Example Code - -```tsx title="src/admin/routes/custom/page.tsx" -import { defineRouteConfig } from "@medusajs/admin-sdk" -import { ChatBubbleLeftRight } from "@medusajs/icons" -import { - Badge, - createDataTableColumnHelper, - createDataTableFilterHelper, - DataTable, - DataTableFilteringState, - DataTablePaginationState, - DataTableSortingState, - Heading, - useDataTable, -} from "@medusajs/ui" -import { useQuery } from "@tanstack/react-query" -import { SingleColumnLayout } from "../../layouts/single-column" -import { sdk } from "../../lib/config" -import { useMemo, useState } from "react" -import { Container } from "../../components/container" -import { HttpTypes, ProductStatus } from "@medusajs/framework/types" - -const columnHelper = createDataTableColumnHelper() - -const columns = [ - columnHelper.accessor("title", { - header: "Title", - // Enables sorting for the column. - enableSorting: true, - // If omitted, the header will be used instead if it's a string, - // otherwise the accessor key (id) will be used. - sortLabel: "Title", - // If omitted the default value will be "A-Z" - sortAscLabel: "A-Z", - // If omitted the default value will be "Z-A" - sortDescLabel: "Z-A", - }), - columnHelper.accessor("status", { - header: "Status", - cell: ({ getValue }) => { - const status = getValue() - return ( - - {status === "published" ? "Published" : "Draft"} - - ) - }, - }), -] - -const filterHelper = createDataTableFilterHelper() - -const filters = [ - filterHelper.accessor("status", { - type: "select", - label: "Status", - options: [ - { - label: "Published", - value: "published", - }, - { - label: "Draft", - value: "draft", - }, - ], - }), -] - -const limit = 15 - -const CustomPage = () => { - const [pagination, setPagination] = useState({ - pageSize: limit, - pageIndex: 0, - }) - const [search, setSearch] = useState("") - const [filtering, setFiltering] = useState({}) - const [sorting, setSorting] = useState(null) - - const offset = useMemo(() => { - return pagination.pageIndex * limit - }, [pagination]) - const statusFilters = useMemo(() => { - return (filtering.status || []) as ProductStatus - }, [filtering]) - - const { data, isLoading } = useQuery({ - queryFn: () => sdk.admin.product.list({ - limit, - offset, - q: search, - status: statusFilters, - order: sorting ? `${sorting.desc ? "-" : ""}${sorting.id}` : undefined, - }), - queryKey: [["products", limit, offset, search, statusFilters, sorting?.id, sorting?.desc]], - }) - - const table = useDataTable({ - columns, - data: data?.products || [], - getRowId: (row) => row.id, - rowCount: data?.count || 0, - isLoading, - pagination: { - state: pagination, - onPaginationChange: setPagination, - }, - search: { - state: search, - onSearchChange: setSearch, - }, - filtering: { - state: filtering, - onFilteringChange: setFiltering, - }, - filters, - sorting: { - // Pass the pagination state and updater to the table instance - state: sorting, - onSortingChange: setSorting, - }, - }) +```tsx title="src/admin/widgets/product-widget.tsx" +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Container } from "../components/container" +import { Header } from "../components/header" +const ProductWidget = () => { return ( - - - - - Products -
- - - -
-
- - -
-
-
+ +
+ ) } -export const config = defineRouteConfig({ - label: "Custom", - icon: ChatBubbleLeftRight, +export const config = defineWidgetConfig({ + zone: "product.details.before", }) -export default CustomPage +export default ProductWidget +``` + +This widget also uses a [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) custom component. + + +# Action Menu - Admin Components + +The Medusa Admin often provides additional actions in a dropdown shown when users click a three-dot icon. + +![Example of an action menu in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1728291319/Medusa%20Resources/action-menu_jnus6k.png) + +To create a component that shows this menu in your customizations, create the file `src/admin/components/action-menu.tsx` with the following content: + +```tsx title="src/admin/components/action-menu.tsx" +import { + DropdownMenu, + IconButton, + clx, +} from "@medusajs/ui" +import { EllipsisHorizontal } from "@medusajs/icons" +import { Link } from "react-router-dom" + +export type Action = { + icon: React.ReactNode + label: string + disabled?: boolean +} & ( + | { + to: string + onClick?: never + } + | { + onClick: () => void + to?: never + } +) + +export type ActionGroup = { + actions: Action[] +} + +export type ActionMenuProps = { + groups: ActionGroup[] +} + +export const ActionMenu = ({ groups }: ActionMenuProps) => { + return ( + + + + + + + + {groups.map((group, index) => { + if (!group.actions.length) { + return null + } + + const isLast = index === groups.length - 1 + + return ( + + {group.actions.map((action, index) => { + if (action.onClick) { + return ( + { + e.stopPropagation() + action.onClick() + }} + className={clx( + "[&_svg]:text-ui-fg-subtle flex items-center gap-x-2", + { + "[&_svg]:text-ui-fg-disabled": action.disabled, + } + )} + > + {action.icon} + {action.label} + + ) + } + + return ( +
+ + e.stopPropagation()}> + {action.icon} + {action.label} + + +
+ ) + })} + {!isLast && } +
+ ) + })} +
+
+ ) +} +``` + +The `ActionMenu` component shows a three-dots icon (or `EllipsisHorizontal`) from the [Medusa Icons package](https://docs.medusajs.com/ui/icons/overview/index.html.md) in a button. + +When the button is clicked, a dropdown menu is shown with the actions passed in the props. + +The component accepts the following props: + +- groups: (\`object\[]\`) Groups of actions to be shown in the dropdown. Each group is separated by a divider. + + - actions: (\`object\[]\`) Actions in the group. + + - icon: (\`React.ReactNode\`) + + - label: (\`string\`) The action's text. + + - disabled: (\`boolean\`) Whether the action is shown as disabled. + + - \`to\`: (\`string\`) The link to take the user to when they click the action. This is required if \`onClick\` isn't provided. + + - \`onClick\`: (\`() => void\`) The function to execute when the action is clicked. This is required if \`to\` isn't provided. + +*** + +## Example + +Use the `ActionMenu` component in any widget or UI route. + +For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content: + +```tsx title="src/admin/widgets/product-widget.tsx" +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Pencil } from "@medusajs/icons" +import { Container } from "../components/container" +import { ActionMenu } from "../components/action-menu" + +const ProductWidget = () => { + return ( + + , + label: "Edit", + onClick: () => { + alert("You clicked the edit action!") + }, + }, + ], + }, + ]} /> + + ) +} + +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` + +This widget also uses a [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) custom component. + +### Use in Header + +You can also use the action menu in the [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) component as part of its actions. + +For example: + +```tsx title="src/admin/widgets/product-widget.tsx" +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Pencil } from "@medusajs/icons" +import { Container } from "../components/container" +import { Header } from "../components/header" + +const ProductWidget = () => { + return ( + +
, + label: "Edit", + onClick: () => { + alert("You clicked the edit action!") + }, + }, + ], + }, + ], + }, + }, + ]} + /> + + ) +} + +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget ``` @@ -54351,289 +54276,492 @@ This component uses the [Container](https://docs.medusajs.com/Users/shahednasser It will add at the top of a product's details page a new section, and in its header you'll find an "Edit Item" button. If you click on it, it will open the drawer with your form. -# Action Menu - Admin Components +# Data Table - Admin Components -The Medusa Admin often provides additional actions in a dropdown shown when users click a three-dot icon. +This component is available after [Medusa v2.4.0+](https://github.com/medusajs/medusa/releases/tag/v2.4.0). -![Example of an action menu in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1728291319/Medusa%20Resources/action-menu_jnus6k.png) +The [DataTable component in Medusa UI](https://docs.medusajs.com/ui/components/data-table/index.html.md) allows you to display data in a table with sorting, filtering, and pagination. It's used across the Medusa Admin dashboard to showcase a list of items, such as a list of products. -To create a component that shows this menu in your customizations, create the file `src/admin/components/action-menu.tsx` with the following content: +![Example of a table in the product listing page](https://res.cloudinary.com/dza7lstvk/image/upload/v1728295658/Medusa%20Resources/list_ddt9zc.png) -```tsx title="src/admin/components/action-menu.tsx" -import { - DropdownMenu, - IconButton, - clx, +You can use this component in your Admin Extensions to display data in a table format, especially if you're retrieving them from API routes of the Medusa application. + +This guide focuses on how to use the `DataTable` component while fetching data from the backend. Refer to the [Medusa UI documentation](https://docs.medusajs.com/ui/components/data-table/index.html.md) for detailed information about the DataTable component and its different usages. + +## Example: DataTable with Data Fetching + +In this example, you'll create a UI widget that shows the list of products retrieved from the [List Products API Route](https://docs.medusajs.com/api/admin#products_getproducts) in a data table with pagination, filtering, searching, and sorting. + +Start by initializing the columns in the data table. To do that, use the `createDataTableColumnHelper` from Medusa UI: + +```tsx title="src/admin/routes/custom/page.tsx" +import { + createDataTableColumnHelper, } from "@medusajs/ui" -import { EllipsisHorizontal } from "@medusajs/icons" -import { Link } from "react-router-dom" +import { + HttpTypes, +} from "@medusajs/framework/types" -export type Action = { - icon: React.ReactNode - label: string - disabled?: boolean -} & ( - | { - to: string - onClick?: never - } - | { - onClick: () => void - to?: never - } +const columnHelper = createDataTableColumnHelper() + +const columns = [ + columnHelper.accessor("title", { + header: "Title", + // Enables sorting for the column. + enableSorting: true, + // If omitted, the header will be used instead if it's a string, + // otherwise the accessor key (id) will be used. + sortLabel: "Title", + // If omitted the default value will be "A-Z" + sortAscLabel: "A-Z", + // If omitted the default value will be "Z-A" + sortDescLabel: "Z-A", + }), + columnHelper.accessor("status", { + header: "Status", + cell: ({ getValue }) => { + const status = getValue() + return ( + + {status === "published" ? "Published" : "Draft"} + + ) + }, + }), +] +``` + +`createDataTableColumnHelper` utility creates a column helper that helps you define the columns for the data table. The column helper has an `accessor` method that accepts two parameters: + +1. The column's key in the table's data. +2. An object with the following properties: + - `header`: The column's header. + - `cell`: (optional) By default, a data's value for a column is displayed as a string. Use this property to specify custom rendering of the value. It accepts a function that returns a string or a React node. The function receives an object that has a `getValue` property function to retrieve the raw value of the cell. + - `enableSorting`: (optional) A boolean that enables sorting data by this column. + - `sortLabel`: (optional) The label for the sorting button. If omitted, the `header` will be used instead if it's a string, otherwise the accessor key (id) will be used. + - `sortAscLabel`: (optional) The label for the ascending sorting button. If omitted, the default value will be "A-Z". + - `sortDescLabel`: (optional) The label for the descending sorting button. If omitted, the default value will be "Z-A". + +Next, you'll define the filters that can be applied to the data table. You'll configure filtering by product status. + +To define the filters, add the following: + +```tsx title="src/admin/routes/custom/page.tsx" +// other imports... +import { + // ... + createDataTableFilterHelper, +} from "@medusajs/ui" + +const filterHelper = createDataTableFilterHelper() + +const filters = [ + filterHelper.accessor("status", { + type: "select", + label: "Status", + options: [ + { + label: "Published", + value: "published", + }, + { + label: "Draft", + value: "draft", + }, + ], + }), +] +``` + +`createDataTableFilterHelper` utility creates a filter helper that helps you define the filters for the data table. The filter helper has an `accessor` method that accepts two parameters: + +1. The key of a column in the table's data. +2. An object with the following properties: + - `type`: The type of filter. It can be either: + - `select`: A select dropdown allowing users to choose multiple values. + - `radio`: A radio button allowing users to choose one value. + - `date`: A date picker allowing users to choose a date. + - `label`: The filter's label. + - `options`: An array of objects with `label` and `value` properties. The `label` is the option's label, and the `value` is the value to filter by. + +You'll now start creating the UI widget's component. Start by adding the necessary state variables: + +```tsx title="src/admin/routes/custom/page.tsx" +// other imports... +import { + // ... + DataTablePaginationState, + DataTableFilteringState, + DataTableSortingState, +} from "@medusajs/ui" +import { useMemo, useState } from "react" + +// ... + +const limit = 15 + +const CustomPage = () => { + const [pagination, setPagination] = useState({ + pageSize: limit, + pageIndex: 0, + }) + const [search, setSearch] = useState("") + const [filtering, setFiltering] = useState({}) + const [sorting, setSorting] = useState(null) + + const offset = useMemo(() => { + return pagination.pageIndex * limit + }, [pagination]) + const statusFilters = useMemo(() => { + return (filtering.status || []) as ProductStatus + }, [filtering]) + + // TODO add data fetching logic +} +``` + +In the component, you've added the following state variables: + +- `pagination`: An object of type `DataTablePaginationState` that holds the pagination state. It has two properties: + - `pageSize`: The number of items to show per page. + - `pageIndex`: The current page index. +- `search`: A string that holds the search query. +- `filtering`: An object of type `DataTableFilteringState` that holds the filtering state. +- `sorting`: An object of type `DataTableSortingState` that holds the sorting state. + +You've also added two memoized variables: + +- `offset`: How many items to skip when fetching data based on the current page. +- `statusFilters`: The selected status filters, if any. + +Next, you'll fetch the products from the Medusa application. Assuming you have the JS SDK configured as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md), add the following imports at the top of the file: + +```tsx title="src/admin/routes/custom/page.tsx" +import { sdk } from "../../lib/config" +import { useQuery } from "@tanstack/react-query" +``` + +This imports the JS SDK instance and `useQuery` from [Tanstack Query](https://tanstack.com/query/latest). + +Then, replace the `TODO` in the component with the following: + +```tsx title="src/admin/routes/custom/page.tsx" +const { data, isLoading } = useQuery({ + queryFn: () => sdk.admin.product.list({ + limit, + offset, + q: search, + status: statusFilters, + order: sorting ? `${sorting.desc ? "-" : ""}${sorting.id}` : undefined, + }), + queryKey: [["products", limit, offset, search, statusFilters, sorting?.id, sorting?.desc]], +}) + +// TODO configure data table +``` + +You use the `useQuery` hook to fetch the products from the Medusa application. In the `queryFn`, you call the `sdk.admin.product.list` method to fetch the products. You pass the following query parameters to the method: + +- `limit`: The number of products to fetch per page. +- `offset`: The number of products to skip based on the current page. +- `q`: The search query, if set. +- `status`: The status filters, if set. +- `order`: The sorting order, if set. + +So, whenever the user changes the current page, search query, status filters, or sorting, the products are fetched based on the new parameters. + +Next, you'll configure the data table. Medusa UI provides a `useDataTable` hook that helps you configure the data table. Add the following imports at the top of the file: + +```tsx title="src/admin/routes/custom/page.tsx" +import { + // ... + useDataTable, +} from "@medusajs/ui" +import { useNavigate } from "react-router-dom" +``` + +Then, replace the `TODO` in the component with the following: + +```tsx title="src/admin/routes/custom/page.tsx" +const navigate = useNavigate() + +const table = useDataTable({ + columns, + data: data?.products || [], + getRowId: (row) => row.id, + rowCount: data?.count || 0, + isLoading, + pagination: { + state: pagination, + onPaginationChange: setPagination, + }, + search: { + state: search, + onSearchChange: setSearch, + }, + filtering: { + state: filtering, + onFilteringChange: setFiltering, + }, + filters, + sorting: { + // Pass the pagination state and updater to the table instance + state: sorting, + onSortingChange: setSorting, + }, + onRowClick: (event, row) => { + // Handle row click, for example + navigate(`/products/${row.id}`) + }, +}) + +// TODO render component +``` + +The `useDataTable` hook accepts an object with the following properties: + +- columns: (\`array\`) The columns to display in the data table. You created this using the \`createDataTableColumnHelper\` utility. +- data: (\`array\`) The products fetched from the Medusa application. +- getRowId: (\`function\`) A function that returns the unique ID of a row. +- rowCount: (\`number\`) The total number of products that can be retrieved. This is used to determine the number of pages. +- isLoading: (\`boolean\`) A boolean that indicates if the data is being fetched. +- pagination: (\`object\`) An object to configure pagination. + + - state: (\`object\`) The pagination React state variable. + + - onPaginationChange: (\`function\`) A function that updates the pagination state. +- search: (\`object\`) An object to configure searching. + + - state: (\`string\`) The search query React state variable. + + - onSearchChange: (\`function\`) A function that updates the search query state. +- filtering: (\`object\`) An object to configure filtering. + + - state: (\`object\`) The filtering React state variable. + + - onFilteringChange: (\`function\`) A function that updates the filtering state. +- filters: (\`array\`) The filters to display in the data table. You created this using the \`createDataTableFilterHelper\` utility. +- sorting: (\`object\`) An object to configure sorting. + + - state: (\`object\`) The sorting React state variable. + + - onSortingChange: (\`function\`) A function that updates the sorting state. +- onRowClick: (\`function\`) A function that allows you to perform an action when the user clicks on a row. In this example, you navigate to the product's detail page. + + - event: (\`mouseevent\`) An instance of the \[MouseClickEvent]\(https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent) object. + + - row: (\`object\`) The data of the row that was clicked. + +Finally, you'll render the data table. But first, add the following imports at the top of the page: + +```tsx title="src/admin/routes/custom/page.tsx" +import { + // ... + DataTable, +} from "@medusajs/ui" +import { SingleColumnLayout } from "../../layouts/single-column" +import { Container } from "../../components/container" +``` + +Aside from the `DataTable` component, you also import the [SingleColumnLayout](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/layouts/single-column/index.html.md) and [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) components implemented in other Admin Component guides. These components ensure a style consistent to other pages in the admin dashboard. + +Then, replace the `TODO` in the component with the following: + +```tsx title="src/admin/routes/custom/page.tsx" +return ( + + + + + Products +
+ + + +
+
+ + +
+
+
) - -export type ActionGroup = { - actions: Action[] -} - -export type ActionMenuProps = { - groups: ActionGroup[] -} - -export const ActionMenu = ({ groups }: ActionMenuProps) => { - return ( - - - - - - - - {groups.map((group, index) => { - if (!group.actions.length) { - return null - } - - const isLast = index === groups.length - 1 - - return ( - - {group.actions.map((action, index) => { - if (action.onClick) { - return ( - { - e.stopPropagation() - action.onClick() - }} - className={clx( - "[&_svg]:text-ui-fg-subtle flex items-center gap-x-2", - { - "[&_svg]:text-ui-fg-disabled": action.disabled, - } - )} - > - {action.icon} - {action.label} - - ) - } - - return ( -
- - e.stopPropagation()}> - {action.icon} - {action.label} - - -
- ) - })} - {!isLast && } -
- ) - })} -
-
- ) -} ``` -The `ActionMenu` component shows a three-dots icon (or `EllipsisHorizontal`) from the [Medusa Icons package](https://docs.medusajs.com/ui/icons/overview/index.html.md) in a button. +You render the `DataTable` component and pass the `table` instance as a prop. In the `DataTable` component, you render a toolbar showing a heading, filter menu, sorting menu, and a search input. You also show pagination after the table. -When the button is clicked, a dropdown menu is shown with the actions passed in the props. +Lastly, export the component and the UI widget's configuration at the end of the file: -The component accepts the following props: +```tsx title="src/admin/routes/custom/page.tsx" +// other imports... +import { defineRouteConfig } from "@medusajs/admin-sdk" +import { ChatBubbleLeftRight } from "@medusajs/icons" -- groups: (\`object\[]\`) Groups of actions to be shown in the dropdown. Each group is separated by a divider. +// ... - - actions: (\`object\[]\`) Actions in the group. - - - icon: (\`React.ReactNode\`) - - - label: (\`string\`) The action's text. - - - disabled: (\`boolean\`) Whether the action is shown as disabled. - - - \`to\`: (\`string\`) The link to take the user to when they click the action. This is required if \`onClick\` isn't provided. - - - \`onClick\`: (\`() => void\`) The function to execute when the action is clicked. This is required if \`to\` isn't provided. - -*** - -## Example - -Use the `ActionMenu` component in any widget or UI route. - -For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content: - -```tsx title="src/admin/widgets/product-widget.tsx" -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Pencil } from "@medusajs/icons" -import { Container } from "../components/container" -import { ActionMenu } from "../components/action-menu" - -const ProductWidget = () => { - return ( - - , - label: "Edit", - onClick: () => { - alert("You clicked the edit action!") - }, - }, - ], - }, - ]} /> - - ) -} - -export const config = defineWidgetConfig({ - zone: "product.details.before", +export const config = defineRouteConfig({ + label: "Custom", + icon: ChatBubbleLeftRight, }) -export default ProductWidget +export default CustomPage ``` -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. +If you start your Medusa application and go to `localhost:9000/app/custom`, you'll see the data table showing the list of products with pagination, filtering, searching, and sorting functionalities. -### Use in Header +### Full Example Code -You can also use the action menu in the [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) component as part of its actions. - -For example: - -```tsx title="src/admin/widgets/product-widget.tsx" -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Pencil } from "@medusajs/icons" -import { Container } from "../components/container" -import { Header } from "../components/header" - -const ProductWidget = () => { - return ( - -
, - label: "Edit", - onClick: () => { - alert("You clicked the edit action!") - }, - }, - ], - }, - ], - }, - }, - ]} - /> - - ) -} - -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) - -export default ProductWidget -``` - - -# Container - Admin Components - -The Medusa Admin wraps each section of a page in a container. - -![Example of a container in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1728287102/Medusa%20Resources/container_soenir.png) - -To create a component that uses the same container styling in your widgets or UI routes, create the file `src/admin/components/container.tsx` with the following content: - -```tsx +```tsx title="src/admin/routes/custom/page.tsx" +import { defineRouteConfig } from "@medusajs/admin-sdk" +import { ChatBubbleLeftRight } from "@medusajs/icons" import { - Container as UiContainer, - clx, + Badge, + createDataTableColumnHelper, + createDataTableFilterHelper, + DataTable, + DataTableFilteringState, + DataTablePaginationState, + DataTableSortingState, + Heading, + useDataTable, } from "@medusajs/ui" +import { useQuery } from "@tanstack/react-query" +import { SingleColumnLayout } from "../../layouts/single-column" +import { sdk } from "../../lib/config" +import { useMemo, useState } from "react" +import { Container } from "../../components/container" +import { HttpTypes, ProductStatus } from "@medusajs/framework/types" -type ContainerProps = React.ComponentProps +const columnHelper = createDataTableColumnHelper() + +const columns = [ + columnHelper.accessor("title", { + header: "Title", + // Enables sorting for the column. + enableSorting: true, + // If omitted, the header will be used instead if it's a string, + // otherwise the accessor key (id) will be used. + sortLabel: "Title", + // If omitted the default value will be "A-Z" + sortAscLabel: "A-Z", + // If omitted the default value will be "Z-A" + sortDescLabel: "Z-A", + }), + columnHelper.accessor("status", { + header: "Status", + cell: ({ getValue }) => { + const status = getValue() + return ( + + {status === "published" ? "Published" : "Draft"} + + ) + }, + }), +] + +const filterHelper = createDataTableFilterHelper() + +const filters = [ + filterHelper.accessor("status", { + type: "select", + label: "Status", + options: [ + { + label: "Published", + value: "published", + }, + { + label: "Draft", + value: "draft", + }, + ], + }), +] + +const limit = 15 + +const CustomPage = () => { + const [pagination, setPagination] = useState({ + pageSize: limit, + pageIndex: 0, + }) + const [search, setSearch] = useState("") + const [filtering, setFiltering] = useState({}) + const [sorting, setSorting] = useState(null) + + const offset = useMemo(() => { + return pagination.pageIndex * limit + }, [pagination]) + const statusFilters = useMemo(() => { + return (filtering.status || []) as ProductStatus + }, [filtering]) + + const { data, isLoading } = useQuery({ + queryFn: () => sdk.admin.product.list({ + limit, + offset, + q: search, + status: statusFilters, + order: sorting ? `${sorting.desc ? "-" : ""}${sorting.id}` : undefined, + }), + queryKey: [["products", limit, offset, search, statusFilters, sorting?.id, sorting?.desc]], + }) + + const table = useDataTable({ + columns, + data: data?.products || [], + getRowId: (row) => row.id, + rowCount: data?.count || 0, + isLoading, + pagination: { + state: pagination, + onPaginationChange: setPagination, + }, + search: { + state: search, + onSearchChange: setSearch, + }, + filtering: { + state: filtering, + onFilteringChange: setFiltering, + }, + filters, + sorting: { + // Pass the pagination state and updater to the table instance + state: sorting, + onSortingChange: setSorting, + }, + }) -export const Container = (props: ContainerProps) => { return ( - - ) -} -``` - -The `Container` component re-uses the component from the [Medusa UI package](https://docs.medusajs.com/ui/components/container/index.html.md) and applies to it classes to match the Medusa Admin's design conventions. - -*** - -## Example - -Use that `Container` component in any widget or UI route. - -For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content: - -```tsx title="src/admin/widgets/product-widget.tsx" -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Container } from "../components/container" -import { Header } from "../components/header" - -const ProductWidget = () => { - return ( - -
- + + + + + Products +
+ + + +
+
+ + +
+
+
) } -export const config = defineWidgetConfig({ - zone: "product.details.before", +export const config = defineRouteConfig({ + label: "Custom", + icon: ChatBubbleLeftRight, }) -export default ProductWidget +export default CustomPage ``` -This widget also uses a [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) custom component. - # Header - Admin Components @@ -55009,98 +55137,6 @@ export default ProductWidget This shows the JSON section at the top of the product page, passing it the object `{ name: "John" }`. -# Section Row - Admin Components - -The Medusa Admin often shows information in rows of label-values, such as when showing a product's details. - -![Example of a section row in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1728292781/Medusa%20Resources/section-row_kknbnw.png) - -To create a component that shows information in the same structure, create the file `src/admin/components/section-row.tsx` with the following content: - -```tsx title="src/admin/components/section-row.tsx" -import { Text, clx } from "@medusajs/ui" - -export type SectionRowProps = { - title: string - value?: React.ReactNode | string | null - actions?: React.ReactNode -} - -export const SectionRow = ({ title, value, actions }: SectionRowProps) => { - const isValueString = typeof value === "string" || !value - - return ( -
- - {title} - - - {isValueString ? ( - - {value ?? "-"} - - ) : ( -
{value}
- )} - - {actions &&
{actions}
} -
- ) -} -``` - -The `SectionRow` component shows a title and a value in the same row. - -It accepts the following props: - -- title: (\`string\`) The title to show on the left side. -- value: (\`React.ReactNode\` \\| \`string\` \\| \`null\`) The value to show on the right side. -- actions: (\`React.ReactNode\`) The actions to show at the end of the row. - -*** - -## Example - -Use the `SectionRow` component in any widget or UI route. - -For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content: - -```tsx title="src/admin/widgets/product-widget.tsx" -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Container } from "../components/container" -import { Header } from "../components/header" -import { SectionRow } from "../components/section-row" - -const ProductWidget = () => { - return ( - -
- - - ) -} - -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) - -export default ProductWidget -``` - -This widget also uses the [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) and [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) custom component. - - # Table - Admin Components If you're using [Medusa v2.4.0+](https://github.com/medusajs/medusa/releases/tag/v2.4.0), it's recommended to use the [Data Table](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/data-table/index.html.md) component instead as it provides features for sorting, filtering, pagination, and more with a simpler API. @@ -55391,6 +55427,98 @@ If `data` isn't `undefined`, you display the `Table` component passing it the fo To test it out, log into the Medusa Admin and open `http://localhost:9000/app/custom`. You'll find a table of products with pagination. +# Section Row - Admin Components + +The Medusa Admin often shows information in rows of label-values, such as when showing a product's details. + +![Example of a section row in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1728292781/Medusa%20Resources/section-row_kknbnw.png) + +To create a component that shows information in the same structure, create the file `src/admin/components/section-row.tsx` with the following content: + +```tsx title="src/admin/components/section-row.tsx" +import { Text, clx } from "@medusajs/ui" + +export type SectionRowProps = { + title: string + value?: React.ReactNode | string | null + actions?: React.ReactNode +} + +export const SectionRow = ({ title, value, actions }: SectionRowProps) => { + const isValueString = typeof value === "string" || !value + + return ( +
+ + {title} + + + {isValueString ? ( + + {value ?? "-"} + + ) : ( +
{value}
+ )} + + {actions &&
{actions}
} +
+ ) +} +``` + +The `SectionRow` component shows a title and a value in the same row. + +It accepts the following props: + +- title: (\`string\`) The title to show on the left side. +- value: (\`React.ReactNode\` \\| \`string\` \\| \`null\`) The value to show on the right side. +- actions: (\`React.ReactNode\`) The actions to show at the end of the row. + +*** + +## Example + +Use the `SectionRow` component in any widget or UI route. + +For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content: + +```tsx title="src/admin/widgets/product-widget.tsx" +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Container } from "../components/container" +import { Header } from "../components/header" +import { SectionRow } from "../components/section-row" + +const ProductWidget = () => { + return ( + +
+ + + ) +} + +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` + +This widget also uses the [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) and [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) custom component. + + # Service Factory Reference This section of the documentation provides a reference of the methods generated for services extending the service factory (`MedusaService`), and how to use them. @@ -55418,162 +55546,300 @@ Some examples of method names: The reference uses only the operation name to refer to the method. -# delete Method - Service Factory Reference +# create Method - Service Factory Reference -This method deletes one or more records. +This method creates one or more records of the data model. -## Delete One Record +## Create 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({ +const post = await postModuleService.createPosts({ 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 retrieve records matching a set of filters, pass an object of the filters as a first parameter. - -Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md). - -### Returns - -The method returns an array of the first `15` records matching the filters. - -*** - -## Retrieve Relations - -This applies to relations between data models of the same module. To retrieve linked records of different modules, use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md). - -```ts -const posts = await postModuleService.listPosts({}, { - relations: ["author"], -}) -``` - -### Parameters - -To retrieve records with their relations, pass as a second parameter an object having a `relations` property. `relations`'s value is an array of relation names. - -### Returns - -The method returns an array of the first `15` records matching the filters. - -*** - -## Select Properties - -```ts -const posts = await postModuleService.listPosts({}, { - select: ["id", "name"], -}) -``` - -### Parameters - -By default, retrieved records have all their properties. To select specific properties to retrieve, pass in the second object parameter a `select` property. - -`select`'s value is an array of property names to retrieve. - -### Returns - -The method returns an array of the first `15` records matching the filters. - -*** - -## Paginate Relations - -```ts -const posts = await postModuleService.listPosts({}, { - take: 20, - skip: 10, -}) -``` - -### Parameters - -To paginate the returned records, the second object parameter accepts the following properties: - -- `take`: a number indicating how many records to retrieve. By default, it's `15`. -- `skip`: a number indicating how many records to skip before the retrieved records. By default, it's `0`. - -### Returns - -The method returns an array of records. The number of records is less than or equal to `take`'s value. - -*** - -## Sort Records - -```ts -const posts = await postModuleService.listPosts({}, { - order: { - name: "ASC", + published_at: new Date(), + metadata: { + external_id: "1234", }, }) ``` -### Parameters +If an object is passed of the method, an object of the created record is also returned. -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. +## Create Multiple Records -### Returns +```ts +const posts = await postModuleService.createPosts([ + { + name: "My Post", + published_at: new Date(), + }, + { + name: "My Other Post", + published_at: new Date(), + }, +]) +``` -The method returns an array of the first `15` records matching the filters. +If an array is passed of the method, an array of the created records is also returned. + + +# 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. + +This guide provides examples of using filters. + +The `list` method is used in the example snippets of this reference, but you can use the same filtering mechanism with any method that accepts filters. + +*** + +## Match Exact Value + +```ts +const posts = await postModuleService.listPosts({ + title: "My Post 2", +}) +``` + +If you pass a property with its value, only records whose properties exactly match the value are selected. + +In the example above, only posts having the title `My Post 2` are retrieved. + +*** + +## Doesn't Match Exact Value + +```ts +const posts = await postModuleService.listPosts({ + title: { + $ne: "My Post", + }, +}) +``` + +To find records with a property that doesn't match a value, pass an object with a `$ne` property. Its value is the value that a record's property shouldn't match. + +In the example above, only posts that don't have the title `My Post` are retrieved. + +*** + +## Match Multiple Values + +```ts +const posts = await postModuleService.listPosts({ + views: [ + 50, + 100, + ], +}) +``` + +To find records with a property matching multiple values, pass an array of those values as the property's value in the filter. + +In the example above, only posts having either `50` or `100` views are retrieved. + +*** + +## Don't Match Multiple Values + +```ts +const posts = await postModuleService.listPosts({ + title: { + $nin: [ + "My Post", + ], + }, +}) +``` + +To find records with a property that doesn't match one or more values, pass an object with a `$nin` property. Its value is an array of multiple values that a record's property shouldn't match. + +In the example above, only posts that don't have the title `My Post` are retrieved. + +*** + +## Match Text Like Value + +This filter only applies to text-like properties, including `text`, `id`, and `enum` properties. + +```ts +const posts = await postModuleService.listPosts({ + title: { + $like: "My%", + }, +}) +``` + +To perform a `like` filter on a record's property, set the property's value to an object with a `$like` property. Its value is the string to use when applying the `like` filter. + +The example above matches all posts whose title starts with `My`. + +*** + +## Filter by Null or Not Null + +To retrieve records with a property that is `null`, set the property's value to `null`. + +For example: + +```ts +const posts = await postModuleService.listPosts({ + published_at: null, +}) +``` + +In the example above, only posts that have a `null` publish date are retrieved. + +On the other hand, to retrieve records with a property that isn't `null`, set the property's value to an object with a `$ne` property. + +For example: + +```ts +const posts = await postModuleService.listPosts({ + published_at: { + $ne: null, + }, +}) +``` + +In the example above, only posts that have a publish date are retrieved. + +*** + +## Apply Range Filters + +This filter only applies to the `number` and `dateTime` properties. + +```ts +const posts = await postModuleService.listPosts({ + published_at: { + $lt: new Date(), + }, +}) +``` + +To filter a record's property to be within a range, set the property's value to an object with any of the following properties: + +1. `$lt`: The property's value must be less than the supplied value. +2. `$lte`: The property's value must be less than or equal to the supplied value. +3. `$gt`: The property's value must be greater than the supplied value. +4. `$gte`: The property's value must be greater than or equal the supplied value. + +In the example above, only posts whose `published_at` property is before the current date and time are retrieved. + +### Example: Retrieve Posts Published Today + +```ts +const startToday = new Date() +startToday.setHours(0, 0, 0, 0) + +const endToday = new Date() +endToday.setHours(23, 59, 59, 59) + +const posts = await postModuleService.listPosts({ + published_at: { + $gte: startToday, + $lte: endToday, + }, +}) +``` + +The `dateTime` property also stores the time. So, when matching for an exact day, you must set a range filter to be between the beginning and end of the day. + +In this example, you retrieve the current date twice: once to set its time to `00:00:00`, and another to set its time `23:59:59`. Then, you retrieve posts whose `published_at` property is between `00:00:00` and `23:59:59` of today. + +### Example: Range Filter on Number Property + +```ts +const posts = await postModuleService.listPosts({ + views: { + $gte: 50, + $lte: 100, + }, +}) +``` + +In the example above, only posts with `views` between `50` and `100` are retrieved. + +*** + +## Apply Or Condition + +```ts +const posts = await postModuleService.listPosts({ + $or: [ + { + title: "My Post", + }, + { + published_at: { + $lt: new Date(), + }, + }, + ], +}) +``` + +To use an `or` condition, pass to the filter object the `$or` property, whose value is an array of filters. + +In the example above, posts whose title is `My Post` or their `published_at` date is less than the current date and time are retrieved. + +*** + +## Apply And Condition + +```ts +const posts = await postModuleService.listPosts({ + $and: [ + { + title: "My Post", + }, + { + published_at: { + $lt: new Date(), + }, + }, + ], +}) +``` + +To use an `and` condition, pass to the filter object the `$and` property, whose value is an array of filters. + +In the example above, only posts whose title is `My Post` and their `published_at` date is less than the current date and time are retrieved. + +*** + +## Complex Filters Example + +```ts +const posts = await postModuleService.listPosts({ + $or: [ + { + $and: [ + { views: { $gte: 50 } }, + { + published_at: { + $gte: new Date(new Date().getFullYear(), 0, 1), + }, + }, + ], + }, + { + title: { + $like: "%Featured%", + }, + }, + ], +}) +``` + +In the example above, posts are retrieved if they meet either of the following conditions: + +1. The post has at least `50` views and was published after the beginning of the current year. +2. The post's title contains the word `Featured`. + +By combining `and` and `or` conditions, you can create complex filters to retrieve records that meet specific criteria. # listAndCount Method - Service Factory Reference @@ -55712,42 +55978,162 @@ The method returns an array with two items: 2. The second is the total count of records. -# create Method - Service Factory Reference +# list Method - Service Factory Reference -This method creates one or more records of the data model. +This method retrieves a list of records. -## Create One Record +## Retrieve List of Records ```ts -const post = await postModuleService.createPosts({ - name: "My Post", - published_at: new Date(), - metadata: { - external_id: "1234", +const posts = await postModuleService.listPosts() +``` + +If no parameters are passed, the method returns an array of the first `15` records. + +*** + +## Filter Records + +```ts +const posts = await postModuleService.listPosts({ + id: ["123", "321"], +}) +``` + +### Parameters + +To retrieve records matching a set of filters, pass an object of the filters as a first parameter. + +Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md). + +### Returns + +The method returns an array of the first `15` records matching the filters. + +*** + +## Retrieve Relations + +This applies to relations between data models of the same module. To retrieve linked records of different modules, use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md). + +```ts +const posts = await postModuleService.listPosts({}, { + relations: ["author"], +}) +``` + +### Parameters + +To retrieve records with their relations, pass as a second parameter an object having a `relations` property. `relations`'s value is an array of relation names. + +### Returns + +The method returns an array of the first `15` records matching the filters. + +*** + +## Select Properties + +```ts +const posts = await postModuleService.listPosts({}, { + select: ["id", "name"], +}) +``` + +### Parameters + +By default, retrieved records have all their properties. To select specific properties to retrieve, pass in the second object parameter a `select` property. + +`select`'s value is an array of property names to retrieve. + +### Returns + +The method returns an array of the first `15` records matching the filters. + +*** + +## Paginate Relations + +```ts +const posts = await postModuleService.listPosts({}, { + take: 20, + skip: 10, +}) +``` + +### Parameters + +To paginate the returned records, the second object parameter accepts the following properties: + +- `take`: a number indicating how many records to retrieve. By default, it's `15`. +- `skip`: a number indicating how many records to skip before the retrieved records. By default, it's `0`. + +### Returns + +The method returns an array of records. The number of records is less than or equal to `take`'s value. + +*** + +## Sort Records + +```ts +const posts = await postModuleService.listPosts({}, { + order: { + name: "ASC", }, }) ``` -If an object is passed of the method, an object of the created record is also returned. +### 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. + + +# 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. *** -## Create Multiple Records +## Delete Multiple Records ```ts -const posts = await postModuleService.createPosts([ - { - name: "My Post", - published_at: new Date(), - }, - { - name: "My Other Post", - published_at: new Date(), - }, +await postModuleService.deletePosts([ + "123", + "321", ]) ``` -If an array is passed of the method, an array of the created records is also returned. +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). # restore Method - Service Factory Reference @@ -56104,264 +56490,6 @@ Learn more about accepted filters in [this documentation](https://docs.medusajs. 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. - -This guide provides examples of using filters. - -The `list` method is used in the example snippets of this reference, but you can use the same filtering mechanism with any method that accepts filters. - -*** - -## Match Exact Value - -```ts -const posts = await postModuleService.listPosts({ - title: "My Post 2", -}) -``` - -If you pass a property with its value, only records whose properties exactly match the value are selected. - -In the example above, only posts having the title `My Post 2` are retrieved. - -*** - -## Doesn't Match Exact Value - -```ts -const posts = await postModuleService.listPosts({ - title: { - $ne: "My Post", - }, -}) -``` - -To find records with a property that doesn't match a value, pass an object with a `$ne` property. Its value is the value that a record's property shouldn't match. - -In the example above, only posts that don't have the title `My Post` are retrieved. - -*** - -## Match Multiple Values - -```ts -const posts = await postModuleService.listPosts({ - views: [ - 50, - 100, - ], -}) -``` - -To find records with a property matching multiple values, pass an array of those values as the property's value in the filter. - -In the example above, only posts having either `50` or `100` views are retrieved. - -*** - -## Don't Match Multiple Values - -```ts -const posts = await postModuleService.listPosts({ - title: { - $nin: [ - "My Post", - ], - }, -}) -``` - -To find records with a property that doesn't match one or more values, pass an object with a `$nin` property. Its value is an array of multiple values that a record's property shouldn't match. - -In the example above, only posts that don't have the title `My Post` are retrieved. - -*** - -## Match Text Like Value - -This filter only applies to text-like properties, including `text`, `id`, and `enum` properties. - -```ts -const posts = await postModuleService.listPosts({ - title: { - $like: "My%", - }, -}) -``` - -To perform a `like` filter on a record's property, set the property's value to an object with a `$like` property. Its value is the string to use when applying the `like` filter. - -The example above matches all posts whose title starts with `My`. - -*** - -## Filter by Null or Not Null - -To retrieve records with a property that is `null`, set the property's value to `null`. - -For example: - -```ts -const posts = await postModuleService.listPosts({ - published_at: null, -}) -``` - -In the example above, only posts that have a `null` publish date are retrieved. - -On the other hand, to retrieve records with a property that isn't `null`, set the property's value to an object with a `$ne` property. - -For example: - -```ts -const posts = await postModuleService.listPosts({ - published_at: { - $ne: null, - }, -}) -``` - -In the example above, only posts that have a publish date are retrieved. - -*** - -## Apply Range Filters - -This filter only applies to the `number` and `dateTime` properties. - -```ts -const posts = await postModuleService.listPosts({ - published_at: { - $lt: new Date(), - }, -}) -``` - -To filter a record's property to be within a range, set the property's value to an object with any of the following properties: - -1. `$lt`: The property's value must be less than the supplied value. -2. `$lte`: The property's value must be less than or equal to the supplied value. -3. `$gt`: The property's value must be greater than the supplied value. -4. `$gte`: The property's value must be greater than or equal the supplied value. - -In the example above, only posts whose `published_at` property is before the current date and time are retrieved. - -### Example: Retrieve Posts Published Today - -```ts -const startToday = new Date() -startToday.setHours(0, 0, 0, 0) - -const endToday = new Date() -endToday.setHours(23, 59, 59, 59) - -const posts = await postModuleService.listPosts({ - published_at: { - $gte: startToday, - $lte: endToday, - }, -}) -``` - -The `dateTime` property also stores the time. So, when matching for an exact day, you must set a range filter to be between the beginning and end of the day. - -In this example, you retrieve the current date twice: once to set its time to `00:00:00`, and another to set its time `23:59:59`. Then, you retrieve posts whose `published_at` property is between `00:00:00` and `23:59:59` of today. - -### Example: Range Filter on Number Property - -```ts -const posts = await postModuleService.listPosts({ - views: { - $gte: 50, - $lte: 100, - }, -}) -``` - -In the example above, only posts with `views` between `50` and `100` are retrieved. - -*** - -## Apply Or Condition - -```ts -const posts = await postModuleService.listPosts({ - $or: [ - { - title: "My Post", - }, - { - published_at: { - $lt: new Date(), - }, - }, - ], -}) -``` - -To use an `or` condition, pass to the filter object the `$or` property, whose value is an array of filters. - -In the example above, posts whose title is `My Post` or their `published_at` date is less than the current date and time are retrieved. - -*** - -## Apply And Condition - -```ts -const posts = await postModuleService.listPosts({ - $and: [ - { - title: "My Post", - }, - { - published_at: { - $lt: new Date(), - }, - }, - ], -}) -``` - -To use an `and` condition, pass to the filter object the `$and` property, whose value is an array of filters. - -In the example above, only posts whose title is `My Post` and their `published_at` date is less than the current date and time are retrieved. - -*** - -## Complex Filters Example - -```ts -const posts = await postModuleService.listPosts({ - $or: [ - { - $and: [ - { views: { $gte: 50 } }, - { - published_at: { - $gte: new Date(new Date().getFullYear(), 0, 1), - }, - }, - ], - }, - { - title: { - $like: "%Featured%", - }, - }, - ], -}) -``` - -In the example above, posts are retrieved if they meet either of the following conditions: - -1. The post has at least `50` views and was published after the beginning of the current year. -2. The post's title contains the word `Featured`. - -By combining `and` and `or` conditions, you can create complex filters to retrieve records that meet specific criteria. - -

Just Getting Started?

diff --git a/www/apps/resources/app/commerce-modules/fulfillment/shipping-option/page.mdx b/www/apps/resources/app/commerce-modules/fulfillment/shipping-option/page.mdx index 225b1879da..08b45db2fc 100644 --- a/www/apps/resources/app/commerce-modules/fulfillment/shipping-option/page.mdx +++ b/www/apps/resources/app/commerce-modules/fulfillment/shipping-option/page.mdx @@ -34,6 +34,12 @@ Service zones can be more restrictive, such as restricting to certain cities or 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](../../pricing/price-rules/page.mdx#how-to-set-rules-on-a-price) guide. + + + These rules are represented by the [ShippingOptionRule data model](/references/fulfillment/models/ShippingOptionRule). Its properties define the custom rule: - `attribute`: The name of a property or table that the rule applies to. For example, `customer_group`. diff --git a/www/apps/resources/app/commerce-modules/pricing/price-rules/page.mdx b/www/apps/resources/app/commerce-modules/pricing/price-rules/page.mdx index 891edfefe4..d3cc6b37d5 100644 --- a/www/apps/resources/app/commerce-modules/pricing/price-rules/page.mdx +++ b/www/apps/resources/app/commerce-modules/pricing/price-rules/page.mdx @@ -1,10 +1,19 @@ +--- +tags: + - name: product + label: "Variant Price Rules" + - name: fulfillment + label: "Shipping Option Price Rules" + - concept +--- + export const metadata = { title: `Price Rules`, } # {metadata.title} -In this document, you'll learn about price rules for price sets and price lists. +In this Pricing Module guide, you'll learn about price rules for price sets and price lists, and how to add rules to a price. ## Price Rule @@ -31,3 +40,142 @@ Rules applied to a price list are represented by the [PriceListRule data model]( 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 Set Rules on a Price? + +### Using Workflows + +Medusa uses the Pricing Module to store prices of different resources, such as product variants and shipping options. + +When you manage one of these resources using [Medusa's workflows](../../../medusa-workflows-reference/page.mdx) or using the API routes that use them, you can set rules on a price using the `rules` property of the price object. + +For example, when creating a shipping option using the [createShippingOptionsWorkflow](!resources!/references/medusa-workflows/createShippingOptionsWorkflow) to create a shipping option, you can make the shipping price free based on the cart total: + +export const workflowHighlights = [ + ["19", "rules", "The default price doesn't have rules."], + ["26", "item_total", "Apply the price if the cart or order's total matches the condition."] +] + +```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, + }, + }, + }, + ], + }], + }) +``` + +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. + +### Using Pricing Module's Service + + + +For most use cases, it's recommended to use [workflows](#using-workflows) instead of directly using the module's service. + + + +When adding a price using the [addPrices](!resources!/references/pricing/addPrices) method of the Pricing Module's service, pass the `rules` property to a price object. + +For example: + +```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 set the default price of a resource (for example, a shipping option), to `$10`. You also add a conditioned price that sets the price to `0` when the cart or order's total is greater than or equal to `$100`. + +### How is the Price Rule Applied? + +The [price calculation](../price-calculation/page.mdx) mechanism considers a price applicable when the resource that this price is in matches the specified rules. + +For example, a [cart object](!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. + + diff --git a/www/apps/resources/app/how-to-tutorials/tutorials/loyalty-points/page.mdx b/www/apps/resources/app/how-to-tutorials/tutorials/loyalty-points/page.mdx index 37f239d4c9..602bb848ed 100644 --- a/www/apps/resources/app/how-to-tutorials/tutorials/loyalty-points/page.mdx +++ b/www/apps/resources/app/how-to-tutorials/tutorials/loyalty-points/page.mdx @@ -628,7 +628,7 @@ export const deductStepHighlights = [ ```ts title="src/workflows/steps/deduct-purchase-points.ts" highlights={deductStepHighlights} collapsibleLines="1-7" expandButtonLabel="Show Imports" import { createStep, - StepResponse + StepResponse, } from "@medusajs/framework/workflows-sdk" import { LOYALTY_MODULE } from "../../modules/loyalty" import LoyaltyModuleService from "../../modules/loyalty/service" @@ -641,7 +641,7 @@ type DeductPurchasePointsInput = { export const deductPurchasePointsStep = createStep( "deduct-purchase-points", async ({ - customer_id, amount + customer_id, amount, }: DeductPurchasePointsInput, { container }) => { const loyaltyModuleService: LoyaltyModuleService = container.resolve( LOYALTY_MODULE @@ -658,7 +658,7 @@ export const deductPurchasePointsStep = createStep( return new StepResponse(result, { customer_id, - points: pointsToDeduct + points: pointsToDeduct, }) }, async (data, { container }) => { @@ -748,7 +748,7 @@ export const addPurchaseAsPointsStep = createStep( return new StepResponse(result, { customer_id: input.customer_id, - points: pointsToAdd + points: pointsToAdd, }) }, async (data, { container }) => { @@ -868,18 +868,18 @@ export const handleOrderPointsWorkflow = createWorkflow( "cart.promotions.*", "cart.promotions.rules.*", "cart.promotions.rules.values.*", - "cart.promotions.application_method.*" + "cart.promotions.application_method.*", ], filters: { - id: order_id + id: order_id, }, options: { - throwIfKeyNotFound: true - } + throwIfKeyNotFound: true, + }, }) validateCustomerExistsStep({ - customer: orders[0].customer + customer: orders[0].customer, } as ValidateCustomerExistsStepInput) const loyaltyPointsPromotion = getCartLoyaltyPromoStep({ @@ -893,14 +893,14 @@ export const handleOrderPointsWorkflow = createWorkflow( .then(() => { deductPurchasePointsStep({ customer_id: orders[0].customer!.id, - amount: loyaltyPointsPromotion.application_method!.value as number + amount: loyaltyPointsPromotion.application_method!.value as number, }) updatePromotionsStep([ { id: loyaltyPointsPromotion.id, status: "inactive", - } + }, ]) }) @@ -912,7 +912,7 @@ export const handleOrderPointsWorkflow = createWorkflow( .then(() => { addPurchaseAsPointsStep({ customer_id: orders[0].customer!.id, - amount: orders[0].total + amount: orders[0].total, }) }) } @@ -1056,10 +1056,10 @@ So, to create an API route at the path `/store/customers/me/loyalty-points`, cre import { AuthenticatedMedusaRequest, - MedusaResponse -} from "@medusajs/framework/http"; -import { LOYALTY_MODULE } from "../../../../../modules/loyalty"; -import LoyaltyModuleService from "../../../../../modules/loyalty/service"; + MedusaResponse, +} from "@medusajs/framework/http" +import { LOYALTY_MODULE } from "../../../../../modules/loyalty" +import LoyaltyModuleService from "../../../../../modules/loyalty/service" export async function GET( req: AuthenticatedMedusaRequest, @@ -1405,17 +1405,17 @@ import { createPromotionsStep, updateCartPromotionsWorkflow, updateCartsStep, - useQueryGraphStep + useQueryGraphStep, } from "@medusajs/medusa/core-flows" import { validateCustomerExistsStep, - ValidateCustomerExistsStepInput + ValidateCustomerExistsStepInput, } from "./steps/validate-customer-exists" import { getCartLoyaltyPromoAmountStep, - GetCartLoyaltyPromoAmountStepInput + GetCartLoyaltyPromoAmountStepInput, } from "./steps/get-cart-loyalty-promo-amount" -import { CartData, CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE, } from "../utils/promo" +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" @@ -1433,7 +1433,7 @@ const fields = [ "promotions.rules.values.*", "currency_code", "total", - "metadata" + "metadata", ] export const applyLoyaltyOnCartWorkflow = createWorkflow( @@ -1444,24 +1444,24 @@ export const applyLoyaltyOnCartWorkflow = createWorkflow( entity: "cart", fields, filters: { - id: input.cart_id + id: input.cart_id, }, options: { - throwIfKeyNotFound: true - } + throwIfKeyNotFound: true, + }, }) validateCustomerExistsStep({ - customer: carts[0].customer + customer: carts[0].customer, } as ValidateCustomerExistsStepInput) getCartLoyaltyPromoStep({ cart: carts[0] as unknown as CartData, - throwErrorOn: "found" + throwErrorOn: "found", }) const amount = getCartLoyaltyPromoAmountStep({ - cart: carts[0] + cart: carts[0], } as unknown as GetCartLoyaltyPromoAmountStepInput) // TODO create and apply the promotion on the cart @@ -1491,7 +1491,7 @@ export const prepareLoyaltyPromoDataHighlights = [ ```ts title="src/workflows/apply-loyalty-on-cart.ts" highlights={prepareLoyaltyPromoDataHighlights} const promoToCreate = transform({ carts, - amount + amount, }, (data) => { const randomStr = Math.random().toString(36).substring(2, 8) const uniqueId = ( @@ -1512,8 +1512,8 @@ const promoToCreate = transform({ { attribute: CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE, operator: "eq", - values: [data.carts[0].customer!.id] - } + values: [data.carts[0].customer!.id], + }, ], campaign: { name: uniqueId, @@ -1521,9 +1521,9 @@ const promoToCreate = transform({ campaign_identifier: uniqueId, budget: { type: "usage", - limit: 1 - } - } + limit: 1, + }, + }, } }) @@ -1559,17 +1559,17 @@ export const createLoyaltyPromoStepHighlights = [ ```ts title="src/workflows/apply-loyalty-on-cart.ts" highlights={createLoyaltyPromoStepHighlights} const loyaltyPromo = createPromotionsStep([ - promoToCreate + promoToCreate, ] as CreatePromotionDTO[]) const { metadata, ...updatePromoData } = transform({ carts, promoToCreate, - loyaltyPromo + loyaltyPromo, }, (data) => { const promos = [ ...(data.carts[0].promotions?.map((promo) => promo?.code).filter(Boolean) || []) as string[], - data.promoToCreate.code + data.promoToCreate.code, ] return { @@ -1577,20 +1577,20 @@ const { metadata, ...updatePromoData } = transform({ promo_codes: promos, action: PromotionActions.ADD, metadata: { - loyalty_promo_id: data.loyaltyPromo[0].id - } + loyalty_promo_id: data.loyaltyPromo[0].id, + }, } }) updateCartPromotionsWorkflow.runAsStep({ - input: updatePromoData + input: updatePromoData, }) updateCartsStep([ { id: input.cart_id, - metadata - } + metadata, + }, ]) // retrieve cart with updated promotions @@ -1621,8 +1621,8 @@ 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"; +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +import { applyLoyaltyOnCartWorkflow } from "../../../../../workflows/apply-loyalty-on-cart" export async function POST( req: MedusaRequest, @@ -1633,8 +1633,8 @@ export async function POST( const { result: cart } = await applyLoyaltyOnCartWorkflow(req.scope) .run({ input: { - cart_id - } + cart_id, + }, }) res.json({ cart }) @@ -1808,13 +1808,13 @@ export const removeLoyaltyFromCartWorkflowHighlights = [ import { createWorkflow, transform, - WorkflowResponse + WorkflowResponse, } from "@medusajs/framework/workflows-sdk" import { useQueryGraphStep, updateCartPromotionsWorkflow, updateCartsStep, - updatePromotionsStep + updatePromotionsStep, } from "@medusajs/medusa/core-flows" import { getCartLoyaltyPromoStep } from "./steps/get-cart-loyalty-promo" import { PromotionActions } from "@medusajs/framework/utils" @@ -1833,7 +1833,7 @@ const fields = [ "promotions.rules.values.*", "currency_code", "total", - "metadata" + "metadata", ] export const removeLoyaltyFromCartWorkflow = createWorkflow( @@ -1844,46 +1844,46 @@ export const removeLoyaltyFromCartWorkflow = createWorkflow( entity: "cart", fields, filters: { - id: input.cart_id - } + id: input.cart_id, + }, }) const loyaltyPromo = getCartLoyaltyPromoStep({ cart: carts[0] as unknown as CartData, - throwErrorOn: "not-found" + throwErrorOn: "not-found", }) updateCartPromotionsWorkflow.runAsStep({ input: { cart_id: input.cart_id, promo_codes: [loyaltyPromo.code!], - action: PromotionActions.REMOVE - } + action: PromotionActions.REMOVE, + }, }) const newMetadata = transform({ - carts + carts, }, (data) => { const { loyalty_promo_id, ...rest } = data.carts[0].metadata || {} return { ...rest, - loyalty_promo_id: null + loyalty_promo_id: null, } }) updateCartsStep([ { id: input.cart_id, - metadata: newMetadata - } + metadata: newMetadata, + }, ]) updatePromotionsStep([ { id: loyaltyPromo.id, - status: "inactive" - } + status: "inactive", + }, ]) // retrieve cart with updated promotions @@ -1920,7 +1920,7 @@ To create the API route, add the following in `src/api/store/carts/[id]/loyalty- ```ts title="src/api/store/carts/[id]/loyalty-points/route.ts" // other imports... -import { removeLoyaltyFromCartWorkflow } from "../../../../../workflows/remove-loyalty-from-cart"; +import { removeLoyaltyFromCartWorkflow } from "../../../../../workflows/remove-loyalty-from-cart" // ... export async function DELETE( @@ -1932,8 +1932,8 @@ export async function DELETE( const { result: cart } = await removeLoyaltyFromCartWorkflow(req.scope) .run({ input: { - cart_id - } + cart_id, + }, }) res.json({ cart }) @@ -2033,11 +2033,11 @@ export const completeCartWorkflowHookHighlights = [ ] ```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"; +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 }) => { @@ -2055,13 +2055,13 @@ completeCartWorkflow.hooks.validate( "promotions.rules.*", "promotions.rules.values.*", "promotions.application_method.*", - "metadata" + "metadata", ], filters: { - id: cart.id - } + id: cart.id, + }, }, { - throwIfKeyNotFound: true + throwIfKeyNotFound: true, }) const loyaltyPromo = getCartLoyaltyPromotion( diff --git a/www/apps/resources/generated/edit-dates.mjs b/www/apps/resources/generated/edit-dates.mjs index c85e0ee140..153f83d8ea 100644 --- a/www/apps/resources/generated/edit-dates.mjs +++ b/www/apps/resources/generated/edit-dates.mjs @@ -24,7 +24,7 @@ export const generatedEditDates = { "app/commerce-modules/fulfillment/fulfillment-provider/page.mdx": "2025-02-26T11:21:56.242Z", "app/commerce-modules/fulfillment/item-fulfillment/page.mdx": "2024-10-08T14:38:15.496Z", "app/commerce-modules/fulfillment/module-options/page.mdx": "2024-10-15T12:51:56.118Z", - "app/commerce-modules/fulfillment/shipping-option/page.mdx": "2024-10-08T14:36:02.660Z", + "app/commerce-modules/fulfillment/shipping-option/page.mdx": "2025-04-17T13:51:40.980Z", "app/commerce-modules/fulfillment/page.mdx": "2025-04-17T08:48:19.367Z", "app/commerce-modules/inventory/_events/_events-table/page.mdx": "2024-07-03T19:27:13+03:00", "app/commerce-modules/inventory/_events/page.mdx": "2024-07-03T19:27:13+03:00", @@ -57,7 +57,7 @@ export const generatedEditDates = { "app/commerce-modules/pricing/_events/page.mdx": "2024-07-03T19:27:13+03:00", "app/commerce-modules/pricing/concepts/page.mdx": "2024-10-09T13:37:25.678Z", "app/commerce-modules/pricing/price-calculation/page.mdx": "2024-10-09T13:43:14.038Z", - "app/commerce-modules/pricing/price-rules/page.mdx": "2024-10-09T13:38:47.112Z", + "app/commerce-modules/pricing/price-rules/page.mdx": "2025-04-17T14:31:26.650Z", "app/commerce-modules/pricing/tax-inclusive-pricing/page.mdx": "2024-10-09T13:48:23.261Z", "app/commerce-modules/pricing/page.mdx": "2025-04-17T08:48:29.165Z", "app/commerce-modules/product/_events/_events-table/page.mdx": "2024-07-03T19:27:13+03:00", @@ -6072,7 +6072,7 @@ export const generatedEditDates = { "app/integrations/guides/algolia/page.mdx": "2025-04-17T08:29:01.433Z", "app/integrations/guides/magento/page.mdx": "2025-04-17T08:29:01.500Z", "app/js-sdk/auth/overview/page.mdx": "2025-03-28T08:05:32.622Z", - "app/how-to-tutorials/tutorials/loyalty-points/page.mdx": "2025-04-17T08:48:07.930Z", + "app/how-to-tutorials/tutorials/loyalty-points/page.mdx": "2025-04-17T14:31:04.727Z", "references/js_sdk/admin/Admin/properties/js_sdk.admin.Admin.plugin/page.mdx": "2025-04-11T09:04:55.084Z", "references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.createAddress/page.mdx": "2025-04-11T09:04:54.010Z", "references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.deleteAddress/page.mdx": "2025-04-11T09:04:54.015Z", diff --git a/www/apps/resources/generated/generated-commerce-modules-sidebar.mjs b/www/apps/resources/generated/generated-commerce-modules-sidebar.mjs index 403b92d7fb..336e549786 100644 --- a/www/apps/resources/generated/generated-commerce-modules-sidebar.mjs +++ b/www/apps/resources/generated/generated-commerce-modules-sidebar.mjs @@ -3283,7 +3283,7 @@ const generatedgeneratedCommerceModulesSidebarSidebar = { "isPathHref": true, "type": "category", "title": "Concepts", - "autogenerate_tags": "concept+fulfillment", + "autogenerate_tags": "fulfillment+concept", "autogenerate_as_ref": true, "children": [ { @@ -3326,6 +3326,14 @@ const generatedgeneratedCommerceModulesSidebarSidebar = { "title": "Links to Other Modules", "children": [] }, + { + "loaded": true, + "isPathHref": true, + "type": "ref", + "title": "Shipping Option Price Rules", + "path": "https://docs.medusajs.com/resources/commerce-modules/pricing/price-rules", + "children": [] + }, { "loaded": true, "isPathHref": true, @@ -11018,7 +11026,7 @@ const generatedgeneratedCommerceModulesSidebarSidebar = { "isPathHref": true, "type": "category", "title": "Concepts", - "autogenerate_tags": "concept+product", + "autogenerate_tags": "product+concept", "autogenerate_as_ref": true, "children": [ { @@ -11052,6 +11060,14 @@ const generatedgeneratedCommerceModulesSidebarSidebar = { "title": "Inventory Kits", "path": "https://docs.medusajs.com/resources/commerce-modules/inventory/inventory-kit", "children": [] + }, + { + "loaded": true, + "isPathHref": true, + "type": "ref", + "title": "Variant Price Rules", + "path": "https://docs.medusajs.com/resources/commerce-modules/pricing/price-rules", + "children": [] } ] }, diff --git a/www/apps/resources/sidebars/fulfillment.mjs b/www/apps/resources/sidebars/fulfillment.mjs index 7d2615aa4a..eef7e338c1 100644 --- a/www/apps/resources/sidebars/fulfillment.mjs +++ b/www/apps/resources/sidebars/fulfillment.mjs @@ -21,7 +21,7 @@ export const fulfillmentSidebar = [ { type: "category", title: "Concepts", - autogenerate_tags: "concept+fulfillment", + autogenerate_tags: "fulfillment+concept", autogenerate_as_ref: true, children: [ { diff --git a/www/apps/resources/sidebars/product.mjs b/www/apps/resources/sidebars/product.mjs index 1dba7a5497..f6f8d359da 100644 --- a/www/apps/resources/sidebars/product.mjs +++ b/www/apps/resources/sidebars/product.mjs @@ -16,7 +16,7 @@ export const productSidebar = [ { type: "category", title: "Concepts", - autogenerate_tags: "concept+product", + autogenerate_tags: "product+concept", autogenerate_as_ref: true, children: [ { diff --git a/www/apps/user-guide/app/settings/locations-and-shipping/locations/page.mdx b/www/apps/user-guide/app/settings/locations-and-shipping/locations/page.mdx index e512e2e2d1..32b43a2aaf 100644 --- a/www/apps/user-guide/app/settings/locations-and-shipping/locations/page.mdx +++ b/www/apps/user-guide/app/settings/locations-and-shipping/locations/page.mdx @@ -9,7 +9,7 @@ tags: label: "Manage Shipping Options" --- -import { EllipsisHorizontal, TaxExclusive, TaxInclusive } from "@medusajs/icons" +import { EllipsisHorizontal, TaxExclusive, TaxInclusive, ArrowsPointingOut, Plus } from "@medusajs/icons" export const metadata = { title: `Manage Locations in Medusa Admin`, @@ -133,9 +133,9 @@ To create a shipping option: If you chose a "Calculated" price type, you can click the "Save" button to create the shipping option. Otherwise click Continue to proceed to the Prices step. -In the Prices step, use the [Bulk Editor](../../../tips/bulk-editor/page.mdx) to enter the shipping option's prices for every currency and region in your store. Once you're done, click the Save button. +#### Fixed and Conditional Shipping Option Prices -Learn how you can further manage the shipping option in the [Manage Shipping Options section](#manage-shipping-options). +In the Prices step, use the [Bulk Editor](../../../tips/bulk-editor/page.mdx) to enter the shipping option's prices for every currency and region in your store. @@ -148,6 +148,29 @@ Tax-inclusive pricing is configured in the [currency's](../../store/page.mdx#edi ![Create shipping option form step 2](https://res.cloudinary.com/dza7lstvk/image/upload/v1739983344/User%20Guide/Screenshot_2025-02-19_at_6.42.12_PM_u3dk2g.png) +Shipping option prices can also be conditioned based on the cart's total. For example, you can make the shipping option free if the cart's total exceeds `$100`. So, you can set a fixed price for a shipping option, and then set a conditioned price based on the cart's total. + +To set a conditioned price for the shipping option: + +1. Hover over a cell in the Bulk Editor based on the currency or region you want to set the conditioned price for. +2. Click the icon that appears to open the conditional pricing form. +3. In the form that opens: + - Conditional pricing card will appear, and you can add another one by clicking the "Add price" button. + - Enter in the "Shipping option price" field the price when this condition is satisfied. + - To set the minimum cart total required for this price to apply: + - Click the next to "Minimum cart total". + - In the field that shows, enter the minimum cart total required for this price to apply. + - To set the maximum cart total required for this price to apply: + - Click the next to "Maximum cart total". + - In the field that shows, enter the maximum cart total required for this price to apply. + - Once you're done, click the Save button. + +Once you're done, click the Save button. + +![Conditional shipping option price form](https://res.cloudinary.com/dza7lstvk/image/upload/v1744889619/User%20Guide/Screenshot_2025-04-17_at_2.33.14_PM_wcqinw.png) + +Learn how you can further manage the shipping option in the [Manage Shipping Options section](#manage-shipping-options). + --- ## Manage Service Zones @@ -222,6 +245,7 @@ To edit the prices of shipping options whose price type is Fixed: 2. In the "Pickup" or "Shipping" sections, find the shipping option and click on the icon at its right. 3. Choose "Edit prices" from the dropdown. 4. In the [Bulk Editor](../../../tips/bulk-editor/page.mdx) that opens, you can edit the shipping option's price for every currency and region in your store. + - Refer to the [Fixed and Conditional Shipping Option Prices](#fixed-and-conditional-shipping-option-prices) section to learn how to set a conditional price for the shipping option. 5. Once you're done, click the Save button. diff --git a/www/apps/user-guide/generated/edit-dates.mjs b/www/apps/user-guide/generated/edit-dates.mjs index 65fd9bdf04..6beec82fe3 100644 --- a/www/apps/user-guide/generated/edit-dates.mjs +++ b/www/apps/user-guide/generated/edit-dates.mjs @@ -47,7 +47,7 @@ export const generatedEditDates = { "app/price-lists/manage/page.mdx": "2025-02-19T10:35:49.881Z", "app/price-lists/page.mdx": "2025-02-19T09:51:32.546Z", "app/settings/tax-regions/page.mdx": "2025-02-19T17:33:43.806Z", - "app/settings/locations-and-shipping/locations/page.mdx": "2025-02-25T15:02:02.164Z", + "app/settings/locations-and-shipping/locations/page.mdx": "2025-04-17T13:47:40.883Z", "app/settings/locations-and-shipping/page.mdx": "2025-02-19T17:23:45.824Z", "app/settings/locations-and-shipping/shipping-profiles/page.mdx": "2025-02-19T17:36:46.339Z", "app/settings/product-tags/page.mdx": "2025-02-19T17:36:25.102Z", diff --git a/www/packages/tags/src/tags/concept.ts b/www/packages/tags/src/tags/concept.ts index cc48ebabe1..2c24484f5d 100644 --- a/www/packages/tags/src/tags/concept.ts +++ b/www/packages/tags/src/tags/concept.ts @@ -3,6 +3,10 @@ export const concept = [ "title": "Inventory Kits", "path": "https://docs.medusajs.com/resources/commerce-modules/inventory/inventory-kit" }, + { + "title": "Price Rules", + "path": "https://docs.medusajs.com/resources/commerce-modules/pricing/price-rules" + }, { "title": "Product Shipping Requirement", "path": "https://docs.medusajs.com/resources/commerce-modules/product/selling-products" diff --git a/www/packages/tags/src/tags/fulfillment.ts b/www/packages/tags/src/tags/fulfillment.ts index 9919c65769..8eb17a47a6 100644 --- a/www/packages/tags/src/tags/fulfillment.ts +++ b/www/packages/tags/src/tags/fulfillment.ts @@ -15,6 +15,10 @@ export const fulfillment = [ "title": "Manage Shipping Profiles", "path": "https://docs.medusajs.com/user-guide/settings/locations-and-shipping/shipping-profiles" }, + { + "title": "Shipping Option Price Rules", + "path": "https://docs.medusajs.com/resources/commerce-modules/pricing/price-rules" + }, { "title": "Product Shipping Requirement", "path": "https://docs.medusajs.com/resources/commerce-modules/product/selling-products" diff --git a/www/packages/tags/src/tags/product.ts b/www/packages/tags/src/tags/product.ts index eee374fd32..d3922a5213 100644 --- a/www/packages/tags/src/tags/product.ts +++ b/www/packages/tags/src/tags/product.ts @@ -51,6 +51,10 @@ export const product = [ "title": "Inventory Kits", "path": "https://docs.medusajs.com/resources/commerce-modules/inventory/inventory-kit" }, + { + "title": "Variant Price Rules", + "path": "https://docs.medusajs.com/resources/commerce-modules/pricing/price-rules" + }, { "title": "Extend Product", "path": "https://docs.medusajs.com/resources/commerce-modules/product/extend"