diff --git a/www/apps/book/app/learn/fundamentals/events-and-subscribers/emit-event/page.mdx b/www/apps/book/app/learn/fundamentals/events-and-subscribers/emit-event/page.mdx
index a2101ad827..4c05c92c92 100644
--- a/www/apps/book/app/learn/fundamentals/events-and-subscribers/emit-event/page.mdx
+++ b/www/apps/book/app/learn/fundamentals/events-and-subscribers/emit-event/page.mdx
@@ -162,6 +162,9 @@ The method accepts an object having the following properties:
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:
+
+
+
export const depsHighlight = [
["8", "dependencies", "An array of module registration names to inject into the Module's container."],
]
@@ -182,9 +185,47 @@ module.exports = defineConfig({
})
```
+
+
+
+export const providerHighlights = [
+ ["8", "dependencies", "An array of module registration names to inject into the module and provider's container."],
+]
+
+```ts title="medusa-config.ts" highlights={depsHighlight}
+import { Modules } from "@medusajs/framework/utils"
+
+module.exports = defineConfig({
+ // ...
+ modules: [
+ {
+ resolve: "@medusajs/medusa/payment",
+ dependencies: [
+ Modules.EVENT_BUS,
+ ],
+ options: {
+ providers: [
+ {
+ resolve: "./src/modules/my-provider",
+ id: "my-provider",
+ options: {
+ // ...
+ },
+ },
+ ],
+ },
+ },
+ ],
+})
+```
+
+
+
+
+
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.
+If a module has providers, the dependencies are also injected into the providers' containers.
### Test it Out
diff --git a/www/apps/book/generated/edit-dates.mjs b/www/apps/book/generated/edit-dates.mjs
index f8c5f4d955..8eb75f6432 100644
--- a/www/apps/book/generated/edit-dates.mjs
+++ b/www/apps/book/generated/edit-dates.mjs
@@ -26,7 +26,7 @@ export const generatedEditDates = {
"app/learn/fundamentals/workflows/add-workflow-hook/page.mdx": "2024-12-09T14:42:39.693Z",
"app/learn/fundamentals/events-and-subscribers/data-payload/page.mdx": "2025-05-01T15:30:08.421Z",
"app/learn/fundamentals/workflows/advanced-example/page.mdx": "2024-09-11T10:46:59.975Z",
- "app/learn/fundamentals/events-and-subscribers/emit-event/page.mdx": "2025-03-18T15:09:40.243Z",
+ "app/learn/fundamentals/events-and-subscribers/emit-event/page.mdx": "2025-06-02T14:47:54.394Z",
"app/learn/fundamentals/workflows/conditions/page.mdx": "2025-01-27T08:45:19.027Z",
"app/learn/fundamentals/modules/module-link-directions/page.mdx": "2024-07-24T09:16:01+02:00",
"app/learn/fundamentals/admin/page.mdx": "2025-04-18T10:28:47.328Z",
diff --git a/www/apps/book/public/llms-full.txt b/www/apps/book/public/llms-full.txt
index 74f0b386f5..a98692274c 100644
--- a/www/apps/book/public/llms-full.txt
+++ b/www/apps/book/public/llms-full.txt
@@ -141,76 +141,6 @@ The next chapter covers how you generally deploy the production build.
You can also refer to the [deployment how-to guides](https://docs.medusajs.com/resources/deployment/index.html.md) for platform-specific how-to guides.
-# Medusa Deployment Overview
-
-In this chapter, you’ll learn the general approach to deploying the Medusa application.
-
-## Medusa Project Components
-
-A standard Medusa project is made up of:
-
-- Medusa application: The Medusa server and the Medusa Admin.
-- One or more storefronts
-
-
-
-You deploy the Medusa application, with the server and admin, separately from the storefront.
-
-***
-
-## Deploying the Medusa Application
-
-You must deploy the Medusa application before the storefront, as it connects to the server and won’t work without a deployed Medusa server URL.
-
-The Medusa application must be deployed to a hosting provider supporting Node.js server deployments, such as Railway, DigitalOcean, AWS, Heroku, etc…
-
-
-
-Your server connects to a PostgreSQL database, Redis, and other services relevant for your setup. Most hosting providers support deploying and managing these databases along with your Medusa server (such as Railway and DigitalOcean).
-
-When you deploy your Medusa application, you also deploy the Medusa Admin. For optimal experience, your hosting provider and plan must offer at least 2GB of RAM.
-
-### Deploy Server and Worker Instances
-
-By default, Medusa runs all processes in a single instance. This includes the server that handles incoming requests and the worker that processes background tasks. While this works for development, it’s not optimal for production environments as many background tasks can be long-running or resource-heavy.
-
-Instead, you should deploy two instances:
-
-- A server instance, which handles incoming requests to the application’s API routes.
-- A worker instance, which processes background tasks, including scheduled jobs and subscribers.
-
-You don’t need to set up different projects for each instance. Instead, you can configure the Medusa application to run in different modes based on environment variables.
-
-Learn more about worker modes and how to configure them in the [Worker Mode chapter](https://docs.medusajs.com/learn/production/worker-mode/index.html.md).
-
-### How to Deploy Medusa?
-
-Medusa Cloud is our managed services offering that makes deploying and operating Medusa applications possible without having to worry about configuring, scaling, and maintaining infrastructure. Medusa Cloud hosts your server, Admin dashboard, database, and Redis instance.
-
-With Medusa Cloud, you maintain full customization control as you deploy your own modules and customizations directly from GitHub:
-
-- Push to deploy.
-- Multiple testing environments.
-- Preview environments for new PRs.
-- Test on production-like data.
-
-[Sign up and learn more about Medusa Cloud](https://medusajs.com/pricing)
-
-To self-host Medusa, the [next chapter](https://docs.medusajs.com/learn/deployment/general/index.html.md) explains the general steps to deploy your Medusa application. Refer to [this reference](https://docs.medusajs.com/resources/deployment/index.html.md) to find how-to deployment guides for general and specific hosting providers.
-
-***
-
-## Deploying the Storefront
-
-The storefront is deployed separately from the Medusa application, and the hosting options depend on the tools and frameworks you use to create the storefront.
-
-If you’re using the Next.js Starter storefront, you may deploy the storefront to any hosting provider that supports frontend frameworks, such as Vercel.
-
-Per Vercel’s [license and plans](https://vercel.com/pricing), their free plan can only be used for personal, non-commercial projects. So, you can deploy the storefront on the free plan for development purposes, but for commercial projects, you must update your Vercel plan.
-
-Refer to [this reference](https://docs.medusajs.com/resources/deployment/index.html.md) to find how-to deployment guides for specific hosting providers.
-
-
# Install Medusa
In this chapter, you'll learn how to install and run a Medusa application.
@@ -345,6 +275,76 @@ Refer to [this documentation](https://docs.medusajs.com/learn/update/index.html.
In the next chapters, you'll learn about the architecture of your Medusa application, then learn how to customize your application to build custom features.
+# Medusa Deployment Overview
+
+In this chapter, you’ll learn the general approach to deploying the Medusa application.
+
+## Medusa Project Components
+
+A standard Medusa project is made up of:
+
+- Medusa application: The Medusa server and the Medusa Admin.
+- One or more storefronts
+
+
+
+You deploy the Medusa application, with the server and admin, separately from the storefront.
+
+***
+
+## Deploying the Medusa Application
+
+You must deploy the Medusa application before the storefront, as it connects to the server and won’t work without a deployed Medusa server URL.
+
+The Medusa application must be deployed to a hosting provider supporting Node.js server deployments, such as Railway, DigitalOcean, AWS, Heroku, etc…
+
+
+
+Your server connects to a PostgreSQL database, Redis, and other services relevant for your setup. Most hosting providers support deploying and managing these databases along with your Medusa server (such as Railway and DigitalOcean).
+
+When you deploy your Medusa application, you also deploy the Medusa Admin. For optimal experience, your hosting provider and plan must offer at least 2GB of RAM.
+
+### Deploy Server and Worker Instances
+
+By default, Medusa runs all processes in a single instance. This includes the server that handles incoming requests and the worker that processes background tasks. While this works for development, it’s not optimal for production environments as many background tasks can be long-running or resource-heavy.
+
+Instead, you should deploy two instances:
+
+- A server instance, which handles incoming requests to the application’s API routes.
+- A worker instance, which processes background tasks, including scheduled jobs and subscribers.
+
+You don’t need to set up different projects for each instance. Instead, you can configure the Medusa application to run in different modes based on environment variables.
+
+Learn more about worker modes and how to configure them in the [Worker Mode chapter](https://docs.medusajs.com/learn/production/worker-mode/index.html.md).
+
+### How to Deploy Medusa?
+
+Medusa Cloud is our managed services offering that makes deploying and operating Medusa applications possible without having to worry about configuring, scaling, and maintaining infrastructure. Medusa Cloud hosts your server, Admin dashboard, database, and Redis instance.
+
+With Medusa Cloud, you maintain full customization control as you deploy your own modules and customizations directly from GitHub:
+
+- Push to deploy.
+- Multiple testing environments.
+- Preview environments for new PRs.
+- Test on production-like data.
+
+[Sign up and learn more about Medusa Cloud](https://medusajs.com/pricing)
+
+To self-host Medusa, the [next chapter](https://docs.medusajs.com/learn/deployment/general/index.html.md) explains the general steps to deploy your Medusa application. Refer to [this reference](https://docs.medusajs.com/resources/deployment/index.html.md) to find how-to deployment guides for general and specific hosting providers.
+
+***
+
+## Deploying the Storefront
+
+The storefront is deployed separately from the Medusa application, and the hosting options depend on the tools and frameworks you use to create the storefront.
+
+If you’re using the Next.js Starter storefront, you may deploy the storefront to any hosting provider that supports frontend frameworks, such as Vercel.
+
+Per Vercel’s [license and plans](https://vercel.com/pricing), their free plan can only be used for personal, non-commercial projects. So, you can deploy the storefront on the free plan for development purposes, but for commercial projects, you must update your Vercel plan.
+
+Refer to [this reference](https://docs.medusajs.com/resources/deployment/index.html.md) to find how-to deployment guides for specific hosting providers.
+
+
# Storefront Development
The Medusa application is made up of a Node.js server and an admin dashboard. Storefronts are installed, built, and hosted separately from the Medusa application, giving you the flexibility to choose the frontend tech stack that you and your team are proficient in, and implement unique design systems and user experience.
@@ -1420,6 +1420,245 @@ import { BrandModuleService } from "@/modules/brand/service"
```
+# Build Custom Features
+
+In the upcoming chapters, you'll follow step-by-step guides to build custom features in Medusa. These guides gradually introduce Medusa's concepts to help you understand what they are and how to use them.
+
+By following these guides, you'll add brands to the Medusa application that you can associate with products.
+
+To build a custom feature in Medusa, you need three main tools:
+
+- [Module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md): a package with commerce logic for a single domain. It defines new tables to add to the database, and a class of methods to manage these tables.
+- [Workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md): a tool to perform an operation comprising multiple steps with built-in rollback and retry mechanisms.
+- [API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md): a REST endpoint that exposes commerce features to clients, such as the admin dashboard or a storefront. The API route executes a workflow that implements the commerce feature using modules.
+
+
+
+***
+
+## Next Chapters: Brand Module Example
+
+The next chapters will guide you to:
+
+1. Build a Brand Module that creates a `Brand` data model and provides data-management features.
+2. Add a workflow to create a brand.
+3. Expose an API route that allows admin users to create a brand using the workflow.
+
+
+# Customize Medusa Admin Dashboard
+
+In the previous chapters, you've customized your Medusa application to [add brands](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md), [expose an API route to create brands](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md), and [linked brands to products](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md).
+
+After customizing and extending your application with new features, you may need to provide an interface for admin users to utilize these features. The Medusa Admin dashboard is extendable, allowing you to:
+
+- Insert components, called [widgets](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md), on existing pages.
+- Add new pages, called [UI Routes](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md).
+
+From these customizations, you can send requests to custom API routes, allowing admin users to manage custom resources on the dashboard
+
+***
+
+## Next Chapters: View Brands in Dashboard
+
+In the next chapters, you'll continue with the brands example to:
+
+- Add a new section to the product details page that shows the product's brand.
+- Add a new page in the dashboard that shows all brands in the store.
+
+
+# Extend Core Commerce Features
+
+In the upcoming chapters, you'll learn about the concepts and tools to extend Medusa's core commerce features.
+
+In other commerce platforms, you extend core features and models through hacky workarounds that can introduce unexpected issues and side effects across the platform. It also makes your application difficult to maintain and upgrade in the long run.
+
+The Medusa Framework and orchestration tools mitigate these issues while supporting all your customization needs:
+
+- [Module Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md): Link data models of different modules without building direct dependencies, ensuring that the Medusa application integrates your modules without side effects.
+- [Workflow Hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md): inject custom functionalities into a workflow at predefined points, called hooks. This allows you to perform custom actions as a part of a core workflow without hacky workarounds.
+- [Additional Data in API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/additional-data/index.html.md): Configure core API routes to accept request parameters relevant to your customizations. These parameters are passed to the underlying workflow's hooks, where you can manage your custom data as part of an existing flow.
+
+***
+
+## Next Chapters: Link Brands to Products Example
+
+The next chapters explain how to use the tools mentioned above with step-by-step guides. You'll continue with the [brands example from the previous chapters](https://docs.medusajs.com/learn/customization/custom-features/index.html.md) to:
+
+- Link brands from the custom [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) to products from Medusa's [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md).
+- Extend the core product-creation workflow and the API route that uses it to allow setting the brand of a newly created product.
+- Retrieve a product's associated brand's details.
+
+
+# Integrate Third-Party Systems
+
+Commerce applications often connect to third-party systems that provide additional or specialized features. For example, you may integrate a Content-Management System (CMS) for rich content features, a payment provider to process credit-card payments, and a notification service to send emails.
+
+The Medusa Framework facilitates integrating these systems and orchestrating operations across them, saving you the effort of managing them yourself. You won't find those capabilities in other commerce platforms that in these scenarios become a bottleneck to building customizations and iterating quickly.
+
+In Medusa, you integrate a third-party system by:
+
+1. Creating a module whose service provides the methods to connect to and perform operations in the third-party system.
+2. Building workflows that complete tasks spanning across systems. You use the module that integrates a third-party system in the workflow's steps.
+3. Executing the workflows you built in an [API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md), at a scheduled time, or when an event is emitted.
+
+***
+
+## Next Chapters: Sync Brands Example
+
+In the previous chapters, you've [added brands](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) to your Medusa application. In the next chapters, you will:
+
+1. Integrate a dummy third-party CMS in the Brand Module.
+2. Sync brands to the CMS when a brand is created.
+3. Sync brands from the CMS at a daily schedule.
+
+
+# Customizations Next Steps: Learn the Fundamentals
+
+The previous guides introduced Medusa's different concepts and how you can use them to customize Medusa for a realistic use case, You added brands to your application, linked them to products, customized the admin dashboard, and integrated a third-party CMS.
+
+The next chapters will cover each of these concepts in depth, with the different ways you can use them, their options or configurations, and more advanced features that weren't covered in the previous guides. While you can start building with Medusa, it's highly recommended to follow the next chapters for a better understanding of Medusa's fundamentals.
+
+## Useful Guides
+
+The following guides and references are useful for your development journey:
+
+3. [Commerce Modules](https://docs.medusajs.com/resources/commerce-modules/index.html.md): Browse the list of Commerce Modules in Medusa and their references to learn how to use them.
+4. [Service Factory Reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md): Learn about the methods generated by `MedusaService` with examples.
+5. [Workflows Reference](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md): Browse the list of core workflows and their hooks that are useful for your customizations.
+6. [Admin Injection Zones](https://docs.medusajs.com/resources/admin-widget-injection-zones/index.html.md): Browse the injection zones in the Medusa Admin to learn where you can inject widgets.
+
+***
+
+## More Examples in Recipes
+
+In the [Recipes](https://docs.medusajs.com/resources/recipes/index.html.md) documentation, you'll also find step-by-step guides for different use cases, such as building a marketplace, digital products, and more.
+
+
+# Re-Use Customizations with Plugins
+
+In the previous chapters, you've learned important concepts related to creating modules, implementing commerce features in workflows, exposing those features in API routes, customizing the Medusa Admin dashboard with Admin Extensions, and integrating third-party systems.
+
+You've implemented the brands example within a single Medusa application. However, this approach is not scalable when you want to reuse your customizations across multiple projects.
+
+To reuse your customizations across multiple Medusa applications, such as implementing brands in different projects, you can create a plugin. A plugin is an NPM package that encapsulates your customizations and can be installed in any Medusa application. Plugins can include modules, workflows, API routes, Admin Extensions, and more.
+
+
+
+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).
+
+
+# Configure Instrumentation
+
+In this chapter, you'll learn about observability in Medusa and how to configure instrumentation with OpenTelemetry.
+
+## Observability with OpenTelemtry
+
+Medusa uses [OpenTelemetry](https://opentelemetry.io/) for instrumentation and reporting. When configured, it reports traces for:
+
+- HTTP requests
+- Workflow executions
+- Query usages
+- Database queries and operations
+
+***
+
+## How to Configure Instrumentation in Medusa?
+
+### Prerequisites
+
+- [An exporter to visualize your application's traces, such as Zipkin.](https://zipkin.io/pages/quickstart.html)
+
+### Install Dependencies
+
+Start by installing the following OpenTelemetry dependencies in your Medusa project:
+
+```bash npm2yarn
+npm install @opentelemetry/sdk-node @opentelemetry/resources @opentelemetry/sdk-trace-node @opentelemetry/instrumentation-pg
+```
+
+Also, install the dependencies relevant for the exporter you use. If you're using Zipkin, install the following dependencies:
+
+```bash npm2yarn
+npm install @opentelemetry/exporter-zipkin
+```
+
+### Add instrumentation.ts
+
+Next, create the file `instrumentation.ts` with the following content:
+
+```ts title="instrumentation.ts"
+import { registerOtel } from "@medusajs/medusa"
+import { ZipkinExporter } from "@opentelemetry/exporter-zipkin"
+
+// If using an exporter other than Zipkin, initialize it here.
+const exporter = new ZipkinExporter({
+ serviceName: "my-medusa-project",
+})
+
+export function register() {
+ registerOtel({
+ serviceName: "medusajs",
+ // pass exporter
+ exporter,
+ instrument: {
+ http: true,
+ workflows: true,
+ query: true,
+ },
+ })
+}
+```
+
+In the `instrumentation.ts` file, you export a `register` function that uses Medusa's `registerOtel` utility function. You also initialize an instance of the exporter, such as Zipkin, and pass it to the `registerOtel` function.
+
+`registerOtel` accepts an object where you can pass any [NodeSDKConfiguration](https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_sdk_node.NodeSDKConfiguration.html) property along with the following properties:
+
+The `NodeSDKConfiguration` properties are accepted since Medusa v2.5.1.
+
+- serviceName: (\`string\`) The name of the service traced.
+- exporter: (\[SpanExporter]\(https://open-telemetry.github.io/opentelemetry-js/interfaces/\_opentelemetry\_sdk\_trace\_base.SpanExporter.html)) An instance of an exporter, such as Zipkin.
+- instrument: (\`object\`) Options specifying what to trace.
+
+ - http: (\`boolean\`) Whether to trace HTTP requests.
+
+ - query: (\`boolean\`) Whether to trace Query usages.
+
+ - workflows: (\`boolean\`) Whether to trace Workflow executions.
+
+ - db: (\`boolean\`) Whether to trace database queries and operations.
+- instrumentations: (\[Instrumentation\[]]\(https://open-telemetry.github.io/opentelemetry-js/interfaces/\_opentelemetry\_instrumentation.Instrumentation.html)) Additional instrumentation options that OpenTelemetry accepts.
+
+***
+
+## Test it Out
+
+To test it out, start your exporter, such as Zipkin.
+
+Then, start your Medusa application:
+
+```bash npm2yarn
+npm run dev
+```
+
+Try to open the Medusa Admin or send a request to an API route.
+
+If you check traces in your exporter, you'll find new traces reported.
+
+### Trace Span Names
+
+Trace span names start with the following keywords based on what it's reporting:
+
+- `{methodName} {URL}` when reporting HTTP requests, where `{methodName}` is the HTTP method, and `{URL}` is the URL the request is sent to.
+- `route:` when reporting route handlers running on an HTTP request.
+- `middleware:` when reporting a middleware running on an HTTP request.
+- `workflow:` when reporting a workflow execution.
+- `step:` when reporting a step in a workflow execution.
+- `query.graph:` when reporting Query usages.
+- `pg.query:` when reporting database queries and operations.
+
+
# Logging
In this chapter, you’ll learn how to use Medusa’s logging utility.
@@ -1658,627 +1897,6 @@ Medusa's Testing Framework works for integration tests only. You can write unit
The next chapters explain how to use the testing tools provided by `@medusajs/test-utils` to write tests.
-# Configure Instrumentation
-
-In this chapter, you'll learn about observability in Medusa and how to configure instrumentation with OpenTelemetry.
-
-## Observability with OpenTelemtry
-
-Medusa uses [OpenTelemetry](https://opentelemetry.io/) for instrumentation and reporting. When configured, it reports traces for:
-
-- HTTP requests
-- Workflow executions
-- Query usages
-- Database queries and operations
-
-***
-
-## How to Configure Instrumentation in Medusa?
-
-### Prerequisites
-
-- [An exporter to visualize your application's traces, such as Zipkin.](https://zipkin.io/pages/quickstart.html)
-
-### Install Dependencies
-
-Start by installing the following OpenTelemetry dependencies in your Medusa project:
-
-```bash npm2yarn
-npm install @opentelemetry/sdk-node @opentelemetry/resources @opentelemetry/sdk-trace-node @opentelemetry/instrumentation-pg
-```
-
-Also, install the dependencies relevant for the exporter you use. If you're using Zipkin, install the following dependencies:
-
-```bash npm2yarn
-npm install @opentelemetry/exporter-zipkin
-```
-
-### Add instrumentation.ts
-
-Next, create the file `instrumentation.ts` with the following content:
-
-```ts title="instrumentation.ts"
-import { registerOtel } from "@medusajs/medusa"
-import { ZipkinExporter } from "@opentelemetry/exporter-zipkin"
-
-// If using an exporter other than Zipkin, initialize it here.
-const exporter = new ZipkinExporter({
- serviceName: "my-medusa-project",
-})
-
-export function register() {
- registerOtel({
- serviceName: "medusajs",
- // pass exporter
- exporter,
- instrument: {
- http: true,
- workflows: true,
- query: true,
- },
- })
-}
-```
-
-In the `instrumentation.ts` file, you export a `register` function that uses Medusa's `registerOtel` utility function. You also initialize an instance of the exporter, such as Zipkin, and pass it to the `registerOtel` function.
-
-`registerOtel` accepts an object where you can pass any [NodeSDKConfiguration](https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_sdk_node.NodeSDKConfiguration.html) property along with the following properties:
-
-The `NodeSDKConfiguration` properties are accepted since Medusa v2.5.1.
-
-- serviceName: (\`string\`) The name of the service traced.
-- exporter: (\[SpanExporter]\(https://open-telemetry.github.io/opentelemetry-js/interfaces/\_opentelemetry\_sdk\_trace\_base.SpanExporter.html)) An instance of an exporter, such as Zipkin.
-- instrument: (\`object\`) Options specifying what to trace.
-
- - http: (\`boolean\`) Whether to trace HTTP requests.
-
- - query: (\`boolean\`) Whether to trace Query usages.
-
- - workflows: (\`boolean\`) Whether to trace Workflow executions.
-
- - db: (\`boolean\`) Whether to trace database queries and operations.
-- instrumentations: (\[Instrumentation\[]]\(https://open-telemetry.github.io/opentelemetry-js/interfaces/\_opentelemetry\_instrumentation.Instrumentation.html)) Additional instrumentation options that OpenTelemetry accepts.
-
-***
-
-## Test it Out
-
-To test it out, start your exporter, such as Zipkin.
-
-Then, start your Medusa application:
-
-```bash npm2yarn
-npm run dev
-```
-
-Try to open the Medusa Admin or send a request to an API route.
-
-If you check traces in your exporter, you'll find new traces reported.
-
-### Trace Span Names
-
-Trace span names start with the following keywords based on what it's reporting:
-
-- `{methodName} {URL}` when reporting HTTP requests, where `{methodName}` is the HTTP method, and `{URL}` is the URL the request is sent to.
-- `route:` when reporting route handlers running on an HTTP request.
-- `middleware:` when reporting a middleware running on an HTTP request.
-- `workflow:` when reporting a workflow execution.
-- `step:` when reporting a step in a workflow execution.
-- `query.graph:` when reporting Query usages.
-- `pg.query:` when reporting database queries and operations.
-
-
-# General Medusa Application Deployment Guide
-
-In this document, you'll learn the general steps to deploy your Medusa application. How you apply these steps depend on your chosen hosting provider or platform.
-
-Find how-to guides for specific platforms in [this documentation](https://docs.medusajs.com/resources/deployment/index.html.md).
-
-Want Medusa to manage and maintain your infrastructure? [Sign up and learn more about Medusa Cloud](https://medusajs.com/pricing)
-
-Medusa Cloud is our managed services offering that makes deploying and operating Medusa applications possible without having to worry about configuring, scaling, and maintaining infrastructure. Medusa Cloud hosts your server, Admin dashboard, database, and Redis instance.
-
-With Medusa Cloud, you maintain full customization control as you deploy your own modules and customizations directly from GitHub:
-
-- Push to deploy.
-- Multiple testing environments.
-- Preview environments for new PRs.
-- Test on production-like data.
-
-### Prerequisites
-
-- [Medusa application’s codebase hosted on GitHub repository.](https://docs.medusajs.com/learn/index.html.md)
-
-## What You'll Deploy
-
-When you deploy the Medusa application, you need to deploy the following resources:
-
-1. PostgreSQL database: This is the database that will hold your Medusa application's details.
-2. Redis database: This is the database that will store the Medusa server's session.
-3. Medusa application in [server and worker mode](https://docs.medusajs.com/learn/production/worker-mode/index.html.md), where:
- - The server mode handles incoming API requests and serving the Medusa Admin dashboard.
- - The worker mode handles background tasks, such as scheduled jobs and subscribers.
-
-So, when choosing a hosting provider, make sure it supports deploying these resources. Also, for optimal experience, the hosting provider and plan must offer at least 2GB of RAM.
-
-***
-
-## 1. Configure Medusa Application
-
-### Worker Mode
-
-The `workerMode` configuration determines which mode the Medusa application runs in. When you deploy the Medusa application, you deploy two instances: one in server mode, and one in worker mode.
-
-Learn more about worker mode in the [Worker Module chapter](https://docs.medusajs.com/learn/production/worker-mode/index.html.md).
-
-So, add the following configuration in `medusa-config.ts`:
-
-```ts title="medusa-config.ts"
-module.exports = defineConfig({
- projectConfig: {
- // ...
- workerMode: process.env.MEDUSA_WORKER_MODE as "shared" | "worker" | "server",
- },
-})
-```
-
-Later, you’ll set different values of the `MEDUSA_WORKER_MODE` environment variable for each Medusa application deployment or instance.
-
-### Configure Medusa Admin
-
-The Medusa Admin is served by the Medusa server application. So, you need to disable it in the worker Medusa application only.
-
-To disable the Medusa Admin in the worker Medusa application while keeping it enabled in the server Medusa application, add the following configuration in `medusa-config.ts`:
-
-```ts title="medusa-config.ts"
-module.exports = defineConfig({
- // ...
- admin: {
- disable: process.env.DISABLE_MEDUSA_ADMIN === "true",
- },
-})
-```
-
-Later, you’ll set different values of the `DISABLE_MEDUSA_ADMIN` environment variable for each Medusa application instance.
-
-### Configure Redis URL
-
-The `redisUrl` configuration specifies the connection URL to Redis to store the Medusa server's session.
-
-Learn more in the [Medusa Configuration documentation](https://docs.medusajs.com/learn/configurations/medusa-config#redisurl/index.html.md).
-
-So, add the following configuration in `medusa-config.ts` :
-
-```ts title="medusa-config.ts"
-module.exports = defineConfig({
- projectConfig: {
- // ...
- redisUrl: process.env.REDIS_URL,
- },
-})
-```
-
-***
-
-## 2. Add predeploy Script
-
-Before you start the Medusa application in production, you should always run migrations and sync links.
-
-So, add the following script in `package.json`:
-
-```json
-"scripts": {
- // ...
- "predeploy": "medusa db:migrate"
-},
-```
-
-***
-
-## 3. Install Production Modules and Providers
-
-By default, your Medusa application uses modules and providers useful for development, such as the In-Memory Cache Module or the Local File Module Provider.
-
-It’s highly recommended to instead use modules and providers suitable for production, including:
-
-- [Redis Cache Module](https://docs.medusajs.com/resources/infrastructure-modules/cache/redis/index.html.md)
-- [Redis Event Bus Module](https://docs.medusajs.com/resources/infrastructure-modules/event/redis/index.html.md)
-- [Workflow Engine Redis Module](https://docs.medusajs.com/resources/infrastructure-modules/workflow-engine/redis/index.html.md)
-- [S3 File Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/file/s3/index.html.md) (or other file module providers production-ready).
-- [SendGrid Notification Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/notification/sendgrid/index.html.md) (or other notification module providers production-ready).
-
-Then, add these modules in `medusa-config.ts`:
-
-```ts title="medusa-config.ts"
-import { Modules } from "@medusajs/framework/utils"
-
-module.exports = defineConfig({
- // ...
- modules: [
- {
- resolve: "@medusajs/medusa/cache-redis",
- options: {
- redisUrl: process.env.REDIS_URL,
- },
- },
- {
- resolve: "@medusajs/medusa/event-bus-redis",
- options: {
- redisUrl: process.env.REDIS_URL,
- },
- },
- {
- resolve: "@medusajs/medusa/workflow-engine-redis",
- options: {
- redis: {
- url: process.env.REDIS_URL,
- },
- },
- },
- ],
-})
-```
-
-Check out the [Integrations](https://docs.medusajs.com/resources/integrations/index.html.md) and [Infrastructure Modules](https://docs.medusajs.com/resources/infrastructure-modules/index.html.md) documentation for other modules and providers to use.
-
-***
-
-## 4. Create PostgreSQL and Redis Databases
-
-Your Medusa application must connect to PostgreSQL and Redis databases. So, before you deploy it, create production PostgreSQL and Redis databases.
-
-If your hosting provider doesn't support databases, you can use [Neon for PostgreSQL database hosting](https://neon.tech/), and [Redis Cloud for the Redis database hosting](https://redis.io/cloud/).
-
-After hosting both databases, keep their connection URLs for the next steps.
-
-***
-
-## 5. Deploy Medusa Application in Server Mode
-
-As mentioned earlier, you'll deploy two instances or create two deployments of your Medusa application: one in server mode, and the other in worker mode.
-
-The deployment steps depend on your hosting provider. This section provides the general steps to perform during the deployment.
-
-### Set Environment Variables
-
-When setting the environment variables of the Medusa application, set the following variables:
-
-```bash
-COOKIE_SECRET=supersecret # TODO GENERATE SECURE SECRET
-JWT_SECRET=supersecret # TODO GENERATE SECURE SECRET
-STORE_CORS= # STOREFRONT URL
-ADMIN_CORS= # ADMIN URL
-AUTH_CORS= # STOREFRONT AND ADMIN URLS, SEPARATED BY COMMAS
-DISABLE_MEDUSA_ADMIN=false
-MEDUSA_WORKER_MODE=server
-PORT=9000
-DATABASE_URL= # POSTGRES DATABASE URL
-REDIS_URL= # REDIS DATABASE URL
-```
-
-Where:
-
-- The value of `COOKIE_SECRET` and `JWT_SECRET` must be a randomly generated secret.
-- `STORE_CORS`'s value is the URL of your storefront. If you don’t have it yet, you can skip adding it for now.
-- `ADMIN_CORS`'s value is the URL of the admin dashboard, which is the same as the server Medusa application. You can add it later if you don't currently have it.
-- `AUTH_CORS`'s value is the URLs of any application authenticating users, customers, or other actor types, such as the storefront and admin URLs. The URLs are separated by commas. If you don’t have the URLs yet, you can set its value later.
-- Set `DISABLE_MEDUSA_ADMIN`'s value to `false` so that the admin is built with the server application.
-- Set the PostgreSQL database's connection URL as the value of `DATABASE_URL`
-- Set the Redis database's connection URL as the value of `REDIS_URL`.
-
-Feel free to add any other relevant environment variables, such as for integrations and Infrastructure Modules. If you're using environment variables in your admin customizations, make sure to set them as well, as they're inlined during the build process.
-
-### Set Start Command
-
-The Medusa application's production build, which is created using the `build` command, outputs the Medusa application to `.medusa/server`. So, you must install the dependencies in the `.medusa/server` directory, then run the `start` command in it.
-
-If your hosting provider doesn't support setting a current-working directory, set the start command to the following:
-
-```bash npm2yarn
-cd .medusa/server && npm install && npm run predeploy && npm run start
-```
-
-Notice that you run the `predeploy` command before starting the Medusa application to run migrations and sync links whenever there's an update.
-
-### Set Backend URL in Admin Configuration
-
-The Medusa Admin is built and hosted statically. To send requests to the Medusa server application, you must set the backend URL in the Medusa Admin's configuration.
-
-After you’ve obtained the Medusa application’s URL, add the following configuration to `medusa-config.ts`:
-
-```ts title="medusa-config.ts"
-module.exports = defineConfig({
- // ...
- admin: {
- // ...
- backendUrl: process.env.MEDUSA_BACKEND_URL,
- },
-})
-```
-
-Then, push the changes to the GitHub repository or deployed application.
-
-In your hosting provider, add or modify the following environment variables for the Medusa application in server mode:
-
-```bash
-ADMIN_CORS= # MEDUSA APPLICATION URL
-AUTH_CORS= # ADD MEDUSA APPLICATION URL
-MEDUSA_BACKEND_URL= # URL TO DEPLOYED MEDUSA APPLICATION
-```
-
-Where you set the value of `ADMIN_CORS` and `MEDUSA_BACKEND_URL` to the Medusa application’s URL, and you add the URL to `AUTH_CORS`.
-
-After setting the environment variables, make sure to restart the deployment for the changes to take effect.
-
-Remember to separate URLs in `AUTH_CORS` by commas.
-
-***
-
-## 6. Deploy Medusa Application in Worker Mode
-
-Next, you'll deploy the Medusa application in worker mode.
-
-As explained in the previous section, the deployment steps depend on your hosting provider. This section provides the general steps to perform during the deployment.
-
-### Set Environment Variables
-
-When setting the environment variables of the Medusa application, set the following variables:
-
-```bash
-COOKIE_SECRET=supersecret # TODO GENERATE SECURE SECRET
-JWT_SECRET=supersecret # TODO GENERATE SECURE SECRET
-DISABLE_MEDUSA_ADMIN=true
-MEDUSA_WORKER_MODE=worker
-PORT=9000
-DATABASE_URL= # POSTGRES DATABASE URL
-REDIS_URL= # REDIS DATABASE URL
-```
-
-Where:
-
-- The value of `COOKIE_SECRET` and `JWT_SECRET` must be a randomly generated secret.
-- Set `DISABLE_MEDUSA_ADMIN`'s value to `true` so that the admin isn't built with the worker application.
-- Set the PostgreSQL database's connection URL as the value of `DATABASE_URL`
-- Set the Redis database's connection URL as the value of `REDIS_URL`.
-
-Feel free to add any other relevant environment variables, such as for integrations and Infrastructure Modules.
-
-### Set Start Command
-
-The Medusa application's production build, which is created using the `build` command, outputs the Medusa application to `.medusa/server`. So, you must install the dependencies in the `.medusa/server` directory, then run the `start` command in it.
-
-If your hosting provider doesn't support setting a current-working directory, set the start command to the following:
-
-```bash npm2yarn
-cd .medusa/server && npm install && npm run start
-```
-
-***
-
-## 7. Test Deployed Application
-
-Once the application is deployed and live, go to `/health`, where `` is the URL of the Medusa application in server mode. If the deployment was successful, you’ll see the `OK` response.
-
-The Medusa Admin is also available at `/app`.
-
-***
-
-## Create Admin User
-
-If your hosting provider supports running commands in your Medusa application's directory, run the following command to create an admin user:
-
-```bash
-npx medusa user -e admin-medusa@test.com -p supersecret
-```
-
-Replace the email `admin-medusa@test.com` and password `supersecret` with the credentials you want.
-
-You can use these credentials to log into the Medusa Admin dashboard.
-
-
-# Build Custom Features
-
-In the upcoming chapters, you'll follow step-by-step guides to build custom features in Medusa. These guides gradually introduce Medusa's concepts to help you understand what they are and how to use them.
-
-By following these guides, you'll add brands to the Medusa application that you can associate with products.
-
-To build a custom feature in Medusa, you need three main tools:
-
-- [Module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md): a package with commerce logic for a single domain. It defines new tables to add to the database, and a class of methods to manage these tables.
-- [Workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md): a tool to perform an operation comprising multiple steps with built-in rollback and retry mechanisms.
-- [API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md): a REST endpoint that exposes commerce features to clients, such as the admin dashboard or a storefront. The API route executes a workflow that implements the commerce feature using modules.
-
-
-
-***
-
-## Next Chapters: Brand Module Example
-
-The next chapters will guide you to:
-
-1. Build a Brand Module that creates a `Brand` data model and provides data-management features.
-2. Add a workflow to create a brand.
-3. Expose an API route that allows admin users to create a brand using the workflow.
-
-
-# Customize Medusa Admin Dashboard
-
-In the previous chapters, you've customized your Medusa application to [add brands](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md), [expose an API route to create brands](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md), and [linked brands to products](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md).
-
-After customizing and extending your application with new features, you may need to provide an interface for admin users to utilize these features. The Medusa Admin dashboard is extendable, allowing you to:
-
-- Insert components, called [widgets](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md), on existing pages.
-- Add new pages, called [UI Routes](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md).
-
-From these customizations, you can send requests to custom API routes, allowing admin users to manage custom resources on the dashboard
-
-***
-
-## Next Chapters: View Brands in Dashboard
-
-In the next chapters, you'll continue with the brands example to:
-
-- Add a new section to the product details page that shows the product's brand.
-- Add a new page in the dashboard that shows all brands in the store.
-
-
-# Extend Core Commerce Features
-
-In the upcoming chapters, you'll learn about the concepts and tools to extend Medusa's core commerce features.
-
-In other commerce platforms, you extend core features and models through hacky workarounds that can introduce unexpected issues and side effects across the platform. It also makes your application difficult to maintain and upgrade in the long run.
-
-The Medusa Framework and orchestration tools mitigate these issues while supporting all your customization needs:
-
-- [Module Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md): Link data models of different modules without building direct dependencies, ensuring that the Medusa application integrates your modules without side effects.
-- [Workflow Hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md): inject custom functionalities into a workflow at predefined points, called hooks. This allows you to perform custom actions as a part of a core workflow without hacky workarounds.
-- [Additional Data in API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/additional-data/index.html.md): Configure core API routes to accept request parameters relevant to your customizations. These parameters are passed to the underlying workflow's hooks, where you can manage your custom data as part of an existing flow.
-
-***
-
-## Next Chapters: Link Brands to Products Example
-
-The next chapters explain how to use the tools mentioned above with step-by-step guides. You'll continue with the [brands example from the previous chapters](https://docs.medusajs.com/learn/customization/custom-features/index.html.md) to:
-
-- Link brands from the custom [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) to products from Medusa's [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md).
-- Extend the core product-creation workflow and the API route that uses it to allow setting the brand of a newly created product.
-- Retrieve a product's associated brand's details.
-
-
-# Customizations Next Steps: Learn the Fundamentals
-
-The previous guides introduced Medusa's different concepts and how you can use them to customize Medusa for a realistic use case, You added brands to your application, linked them to products, customized the admin dashboard, and integrated a third-party CMS.
-
-The next chapters will cover each of these concepts in depth, with the different ways you can use them, their options or configurations, and more advanced features that weren't covered in the previous guides. While you can start building with Medusa, it's highly recommended to follow the next chapters for a better understanding of Medusa's fundamentals.
-
-## Useful Guides
-
-The following guides and references are useful for your development journey:
-
-3. [Commerce Modules](https://docs.medusajs.com/resources/commerce-modules/index.html.md): Browse the list of Commerce Modules in Medusa and their references to learn how to use them.
-4. [Service Factory Reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md): Learn about the methods generated by `MedusaService` with examples.
-5. [Workflows Reference](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md): Browse the list of core workflows and their hooks that are useful for your customizations.
-6. [Admin Injection Zones](https://docs.medusajs.com/resources/admin-widget-injection-zones/index.html.md): Browse the injection zones in the Medusa Admin to learn where you can inject widgets.
-
-***
-
-## More Examples in Recipes
-
-In the [Recipes](https://docs.medusajs.com/resources/recipes/index.html.md) documentation, you'll also find step-by-step guides for different use cases, such as building a marketplace, digital products, and more.
-
-
-# Re-Use Customizations with Plugins
-
-In the previous chapters, you've learned important concepts related to creating modules, implementing commerce features in workflows, exposing those features in API routes, customizing the Medusa Admin dashboard with Admin Extensions, and integrating third-party systems.
-
-You've implemented the brands example within a single Medusa application. However, this approach is not scalable when you want to reuse your customizations across multiple projects.
-
-To reuse your customizations across multiple Medusa applications, such as implementing brands in different projects, you can create a plugin. A plugin is an NPM package that encapsulates your customizations and can be installed in any Medusa application. Plugins can include modules, workflows, API routes, Admin Extensions, and more.
-
-
-
-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.
-
-***
-
-## 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.
-
-
-# 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).
-
-
-
-***
-
-## 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).
-
-
-
-***
-
-## 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.
-
-
-
-### Infrastructure Modules
-
-[Infrastructure Modules](https://docs.medusajs.com/resources/infrastructure-modules/index.html.md) integrate third-party services and systems that customize Medusa's infrastructure. Medusa has the following Infrastructure Modules:
-
-- [Analytics Module](https://docs.medusajs.com/resources/infrastructure-modules/analytics/index.html.md): Tracks and analyzes user interactions and system events with third-party analytic providers. You can integrate [PostHog](https://docs.medusajs.com/resources/infrastructure-modules/analytics/posthog/index.html.md) as the analytics provider.
-- [Cache Module](https://docs.medusajs.com/resources/infrastructure-modules/cache/index.html.md): Caches data that require heavy computation. You can integrate a custom module to handle the caching with services like Memcached, or use the existing [Redis Cache Module](https://docs.medusajs.com/resources/infrastructure-modules/cache/redis/index.html.md).
-- [Event Module](https://docs.medusajs.com/resources/infrastructure-modules/event/index.html.md): A pub/sub system that allows you to subscribe to events and trigger them. You can integrate [Redis](https://docs.medusajs.com/resources/infrastructure-modules/event/redis/index.html.md) as the pub/sub system.
-- [File Module](https://docs.medusajs.com/resources/infrastructure-modules/file/index.html.md): Manages file uploads and storage, such as upload of product images. You can integrate [AWS S3](https://docs.medusajs.com/resources/infrastructure-modules/file/s3/index.html.md) for file storage.
-- [Locking Module](https://docs.medusajs.com/resources/infrastructure-modules/locking/index.html.md): Manages access to shared resources by multiple processes or threads, preventing conflict between processes and ensuring data consistency. You can integrate [Redis](https://docs.medusajs.com/resources/infrastructure-modules/locking/redis/index.html.md) for locking.
-- [Notification Module](https://docs.medusajs.com/resources/infrastructure-modules/notification/index.html.md): Sends notifications to customers and users, such as for order updates or newsletters. You can integrate [SendGrid](https://docs.medusajs.com/resources/infrastructure-modules/notification/sendgrid/index.html.md) for sending emails.
-- [Workflow Engine Module](https://docs.medusajs.com/resources/infrastructure-modules/workflow-engine/index.html.md): Orchestrates workflows that hold the business logic of your application. You can integrate [Redis](https://docs.medusajs.com/resources/infrastructure-modules/workflow-engine/redis/index.html.md) to orchestrate workflows.
-
-All of the third-party services mentioned above can be replaced to help you build your preferred architecture and ecosystem.
-
-
-
-***
-
-## Full Diagram of Medusa's Architecture
-
-The following diagram illustrates Medusa's architecture including all its layers.
-
-
-
-
# Admin Development
In this chapter, you'll learn about the Medusa Admin dashboard and the possible ways to customize it.
@@ -2629,6 +2247,156 @@ 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).
+# Medusa Container
+
+In this chapter, you’ll learn about the Medusa container and how to use it.
+
+## What is the Medusa Container?
+
+The Medusa container is a registry of Framework and commerce tools that's accessible across your application. Medusa automatically registers these tools in the container, including custom ones that you've built, so that you can use them in your customizations.
+
+In other platforms, if you have a resource A (for example, a class) that depends on a resource B, you have to manually add resource B to the container or specify it beforehand as A's dependency, which is often done in a file separate from A's code. This becomes difficult to manage as you maintain larger applications with many changing dependencies.
+
+Medusa simplifies this process by giving you access to the container, with the tools or resources already registered, at all times in your customizations. When you reach a point in your code where you need a tool, you resolve it from the container and use it.
+
+For example, consider you're creating an API route that retrieves products based on filters using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md), a tool that fetches data across the application. In the API route's function, you can resolve Query from the container passed to the API route and use it:
+
+```ts highlights={highlights}
+import { MedusaRequest, MedusaResponse } from "@medusajs/framework"
+import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
+
+export async function GET(
+ req: MedusaRequest,
+ res: MedusaResponse
+) {
+ const query = req.scope.resolve("query")
+
+ const { data: products } = await query.graph({
+ entity: "product",
+ fields: ["*"],
+ filters: {
+ id: "prod_123",
+ },
+ })
+
+ res.json({
+ products,
+ })
+}
+```
+
+The API route accepts as a first parameter a request object that has a `scope` property, which is the Medusa container. It has a `resolve` method that resolves a resource from the container by the key it's registered with.
+
+You can learn more about how Query works in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md).
+
+***
+
+## List of Resources Registered in the Medusa Container
+
+Find a full list of the registered resources and their registration key in [this reference](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md)
+
+***
+
+## How to Resolve From the Medusa Container
+
+This section gives quick examples of how to resolve resources from the Medusa container in customizations other than an API route, which is covered in the section above.
+
+### Subscriber
+
+A [subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) function, which is executed when an event is emitted, accepts as a parameter an object with a `container` property, whose value is the Medusa container. Use its `resolve` method to resolve a resource by its registration key:
+
+```ts highlights={subscriberHighlights}
+import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
+import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
+
+export default async function productCreateHandler({
+ event: { data },
+ container,
+}: SubscriberArgs<{ id: string }>) {
+ const query = container.resolve(ContainerRegistrationKeys.QUERY)
+
+ const { data: products } = await query.graph({
+ entity: "product",
+ fields: ["*"],
+ filters: {
+ id: data.id,
+ },
+ })
+
+ console.log(`You created a product with the title ${products[0].title}`)
+}
+
+export const config: SubscriberConfig = {
+ event: `product.created`,
+}
+```
+
+### Scheduled Job
+
+A [scheduled job](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) function, which is executed at a specified interval, accepts the Medusa container as a parameter. Use its `resolve` method to resolve a resource by its registration key:
+
+```ts highlights={scheduledJobHighlights}
+import { MedusaContainer } from "@medusajs/framework/types"
+import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
+
+export default async function myCustomJob(
+ container: MedusaContainer
+) {
+ const query = container.resolve(ContainerRegistrationKeys.QUERY)
+
+ const { data: products } = await query.graph({
+ entity: "product",
+ fields: ["*"],
+ filters: {
+ id: "prod_123",
+ },
+ })
+
+ console.log(
+ `You have ${products.length} matching your filters.`
+ )
+}
+
+export const config = {
+ name: "every-minute-message",
+ // execute every minute
+ schedule: "* * * * *",
+}
+```
+
+### Workflow Step
+
+A [step in a workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), which is a special function where you build durable execution logic across multiple modules, accepts in its second parameter a `container` property, whose value is the Medusa container. Use its `resolve` method to resolve a resource by its registration key:
+
+```ts highlights={workflowStepsHighlight}
+import {
+ createStep,
+ StepResponse,
+} from "@medusajs/framework/workflows-sdk"
+import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
+
+const step1 = createStep("step-1", async (_, { container }) => {
+ const query = container.resolve(ContainerRegistrationKeys.QUERY)
+
+ const { data: products } = await query.graph({
+ entity: "product",
+ fields: ["*"],
+ filters: {
+ id: "prod_123",
+ },
+ })
+
+ return new StepResponse(products)
+})
+```
+
+### Module Services and Loaders
+
+A [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), which is a package of functionalities for a single feature or domain, has its own container, so it can't resolve resources from the Medusa container.
+
+Learn more about the module's container in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md).
+
+
# Framework Overview
In this chapter, you'll learn about the Medusa Framework and how it facilitates building customizations in your Medusa application.
@@ -3399,154 +3167,42 @@ To learn more about the different concepts useful for building plugins, check ou
- [Plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md)
-# Medusa Container
+# Plugins
-In this chapter, you’ll learn about the Medusa container and how to use it.
+In this chapter, you'll learn what a plugin is in Medusa.
-## What is the Medusa Container?
+Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0).
-The Medusa container is a registry of Framework and commerce tools that's accessible across your application. Medusa automatically registers these tools in the container, including custom ones that you've built, so that you can use them in your customizations.
+## What is a Plugin?
-In other platforms, if you have a resource A (for example, a class) that depends on a resource B, you have to manually add resource B to the container or specify it beforehand as A's dependency, which is often done in a file separate from A's code. This becomes difficult to manage as you maintain larger applications with many changing dependencies.
+A plugin is a package of reusable Medusa customizations that you can install in any Medusa application. The supported customizations are [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), [API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md), [Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), [Workflow Hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md), [Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md), [Subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md), [Scheduled Jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md), and [Admin Extensions](https://docs.medusajs.com/learn/fundamentals/admin/index.html.md).
-Medusa simplifies this process by giving you access to the container, with the tools or resources already registered, at all times in your customizations. When you reach a point in your code where you need a tool, you resolve it from the container and use it.
+Plugins allow you to reuse your Medusa customizations across multiple projects or share them with the community. They can be published to npm and installed in any Medusa project.
-For example, consider you're creating an API route that retrieves products based on filters using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md), a tool that fetches data across the application. In the API route's function, you can resolve Query from the container passed to the API route and use it:
+
-```ts highlights={highlights}
-import { MedusaRequest, MedusaResponse } from "@medusajs/framework"
-import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
-
-export async function GET(
- req: MedusaRequest,
- res: MedusaResponse
-) {
- const query = req.scope.resolve("query")
-
- const { data: products } = await query.graph({
- entity: "product",
- fields: ["*"],
- filters: {
- id: "prod_123",
- },
- })
-
- res.json({
- products,
- })
-}
-```
-
-The API route accepts as a first parameter a request object that has a `scope` property, which is the Medusa container. It has a `resolve` method that resolves a resource from the container by the key it's registered with.
-
-You can learn more about how Query works in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md).
+Learn how to create a wishlist plugin in [this guide](https://docs.medusajs.com/resources/plugins/guides/wishlist/index.html.md).
***
-## List of Resources Registered in the Medusa Container
+## Plugin vs Module
-Find a full list of the registered resources and their registration key in [this reference](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md)
+A [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) is an isolated package related to a single domain or functionality, such as product reviews or integrating a Content Management System. A module can't access any resources in the Medusa application that are outside its codebase.
+
+A plugin, on the other hand, can contain multiple Medusa customizations, including modules. Your plugin can define a module, then build flows around it.
+
+For example, in a plugin, you can define a module that integrates a third-party service, then add a workflow that uses the module when a certain event occurs to sync data to that service.
+
+- You want to reuse your Medusa customizations across multiple projects.
+- You want to share your Medusa customizations with the community.
+
+- You want to build a custom feature related to a single domain or integrate a third-party service. Instead, use a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). You can wrap that module in a plugin if it's used in other customizations, such as if it has a module link or it's used in a workflow.
***
-## How to Resolve From the Medusa Container
+## How to Create a Plugin?
-This section gives quick examples of how to resolve resources from the Medusa container in customizations other than an API route, which is covered in the section above.
-
-### Subscriber
-
-A [subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) function, which is executed when an event is emitted, accepts as a parameter an object with a `container` property, whose value is the Medusa container. Use its `resolve` method to resolve a resource by its registration key:
-
-```ts highlights={subscriberHighlights}
-import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
-import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
-
-export default async function productCreateHandler({
- event: { data },
- container,
-}: SubscriberArgs<{ id: string }>) {
- const query = container.resolve(ContainerRegistrationKeys.QUERY)
-
- const { data: products } = await query.graph({
- entity: "product",
- fields: ["*"],
- filters: {
- id: data.id,
- },
- })
-
- console.log(`You created a product with the title ${products[0].title}`)
-}
-
-export const config: SubscriberConfig = {
- event: `product.created`,
-}
-```
-
-### Scheduled Job
-
-A [scheduled job](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) function, which is executed at a specified interval, accepts the Medusa container as a parameter. Use its `resolve` method to resolve a resource by its registration key:
-
-```ts highlights={scheduledJobHighlights}
-import { MedusaContainer } from "@medusajs/framework/types"
-import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
-
-export default async function myCustomJob(
- container: MedusaContainer
-) {
- const query = container.resolve(ContainerRegistrationKeys.QUERY)
-
- const { data: products } = await query.graph({
- entity: "product",
- fields: ["*"],
- filters: {
- id: "prod_123",
- },
- })
-
- console.log(
- `You have ${products.length} matching your filters.`
- )
-}
-
-export const config = {
- name: "every-minute-message",
- // execute every minute
- schedule: "* * * * *",
-}
-```
-
-### Workflow Step
-
-A [step in a workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), which is a special function where you build durable execution logic across multiple modules, accepts in its second parameter a `container` property, whose value is the Medusa container. Use its `resolve` method to resolve a resource by its registration key:
-
-```ts highlights={workflowStepsHighlight}
-import {
- createStep,
- StepResponse,
-} from "@medusajs/framework/workflows-sdk"
-import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
-
-const step1 = createStep("step-1", async (_, { container }) => {
- const query = container.resolve(ContainerRegistrationKeys.QUERY)
-
- const { data: products } = await query.graph({
- entity: "product",
- fields: ["*"],
- filters: {
- id: "prod_123",
- },
- })
-
- return new StepResponse(products)
-})
-```
-
-### Module Services and Loaders
-
-A [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), which is a package of functionalities for a single feature or domain, has its own container, so it can't resolve resources from the Medusa container.
-
-Learn more about the module's container in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md).
+The next chapter explains how you can create and publish a plugin.
# Data Models
@@ -3868,44 +3524,6 @@ npx medusa db:migrate
```
-# Plugins
-
-In this chapter, you'll learn what a plugin is in Medusa.
-
-Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0).
-
-## What is a Plugin?
-
-A plugin is a package of reusable Medusa customizations that you can install in any Medusa application. The supported customizations are [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), [API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md), [Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), [Workflow Hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md), [Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md), [Subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md), [Scheduled Jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md), and [Admin Extensions](https://docs.medusajs.com/learn/fundamentals/admin/index.html.md).
-
-Plugins allow you to reuse your Medusa customizations across multiple projects or share them with the community. They can be published to npm and installed in any Medusa project.
-
-
-
-Learn how to create a wishlist plugin in [this guide](https://docs.medusajs.com/resources/plugins/guides/wishlist/index.html.md).
-
-***
-
-## Plugin vs Module
-
-A [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) is an isolated package related to a single domain or functionality, such as product reviews or integrating a Content Management System. A module can't access any resources in the Medusa application that are outside its codebase.
-
-A plugin, on the other hand, can contain multiple Medusa customizations, including modules. Your plugin can define a module, then build flows around it.
-
-For example, in a plugin, you can define a module that integrates a third-party service, then add a workflow that uses the module when a certain event occurs to sync data to that service.
-
-- You want to reuse your Medusa customizations across multiple projects.
-- You want to share your Medusa customizations with the community.
-
-- You want to build a custom feature related to a single domain or integrate a third-party service. Instead, use a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). You can wrap that module in a plugin if it's used in other customizations, such as if it has a module link or it's used in a workflow.
-
-***
-
-## How to Create a Plugin?
-
-The next chapter explains how you can create and publish a plugin.
-
-
# Modules
In this chapter, you’ll learn about modules and how to create them.
@@ -4300,98 +3918,6 @@ In the scheduled job function, you execute the `syncProductToErpWorkflow` by inv
The next time you start the Medusa application, it will run this job every day at midnight.
-# 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.
-
-
-
-***
-
-## 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.
@@ -4646,6 +4172,480 @@ 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.
+# 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).
+
+
+
+***
+
+## 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).
+
+
+
+***
+
+## 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.
+
+
+
+### Infrastructure Modules
+
+[Infrastructure Modules](https://docs.medusajs.com/resources/infrastructure-modules/index.html.md) integrate third-party services and systems that customize Medusa's infrastructure. Medusa has the following Infrastructure Modules:
+
+- [Analytics Module](https://docs.medusajs.com/resources/infrastructure-modules/analytics/index.html.md): Tracks and analyzes user interactions and system events with third-party analytic providers. You can integrate [PostHog](https://docs.medusajs.com/resources/infrastructure-modules/analytics/posthog/index.html.md) as the analytics provider.
+- [Cache Module](https://docs.medusajs.com/resources/infrastructure-modules/cache/index.html.md): Caches data that require heavy computation. You can integrate a custom module to handle the caching with services like Memcached, or use the existing [Redis Cache Module](https://docs.medusajs.com/resources/infrastructure-modules/cache/redis/index.html.md).
+- [Event Module](https://docs.medusajs.com/resources/infrastructure-modules/event/index.html.md): A pub/sub system that allows you to subscribe to events and trigger them. You can integrate [Redis](https://docs.medusajs.com/resources/infrastructure-modules/event/redis/index.html.md) as the pub/sub system.
+- [File Module](https://docs.medusajs.com/resources/infrastructure-modules/file/index.html.md): Manages file uploads and storage, such as upload of product images. You can integrate [AWS S3](https://docs.medusajs.com/resources/infrastructure-modules/file/s3/index.html.md) for file storage.
+- [Locking Module](https://docs.medusajs.com/resources/infrastructure-modules/locking/index.html.md): Manages access to shared resources by multiple processes or threads, preventing conflict between processes and ensuring data consistency. You can integrate [Redis](https://docs.medusajs.com/resources/infrastructure-modules/locking/redis/index.html.md) for locking.
+- [Notification Module](https://docs.medusajs.com/resources/infrastructure-modules/notification/index.html.md): Sends notifications to customers and users, such as for order updates or newsletters. You can integrate [SendGrid](https://docs.medusajs.com/resources/infrastructure-modules/notification/sendgrid/index.html.md) for sending emails.
+- [Workflow Engine Module](https://docs.medusajs.com/resources/infrastructure-modules/workflow-engine/index.html.md): Orchestrates workflows that hold the business logic of your application. You can integrate [Redis](https://docs.medusajs.com/resources/infrastructure-modules/workflow-engine/redis/index.html.md) to orchestrate workflows.
+
+All of the third-party services mentioned above can be replaced to help you build your preferred architecture and ecosystem.
+
+
+
+***
+
+## Full Diagram of Medusa's Architecture
+
+The following diagram illustrates Medusa's architecture including all its layers.
+
+
+
+
+# General Medusa Application Deployment Guide
+
+In this document, you'll learn the general steps to deploy your Medusa application. How you apply these steps depend on your chosen hosting provider or platform.
+
+Find how-to guides for specific platforms in [this documentation](https://docs.medusajs.com/resources/deployment/index.html.md).
+
+Want Medusa to manage and maintain your infrastructure? [Sign up and learn more about Medusa Cloud](https://medusajs.com/pricing)
+
+Medusa Cloud is our managed services offering that makes deploying and operating Medusa applications possible without having to worry about configuring, scaling, and maintaining infrastructure. Medusa Cloud hosts your server, Admin dashboard, database, and Redis instance.
+
+With Medusa Cloud, you maintain full customization control as you deploy your own modules and customizations directly from GitHub:
+
+- Push to deploy.
+- Multiple testing environments.
+- Preview environments for new PRs.
+- Test on production-like data.
+
+### Prerequisites
+
+- [Medusa application’s codebase hosted on GitHub repository.](https://docs.medusajs.com/learn/index.html.md)
+
+## What You'll Deploy
+
+When you deploy the Medusa application, you need to deploy the following resources:
+
+1. PostgreSQL database: This is the database that will hold your Medusa application's details.
+2. Redis database: This is the database that will store the Medusa server's session.
+3. Medusa application in [server and worker mode](https://docs.medusajs.com/learn/production/worker-mode/index.html.md), where:
+ - The server mode handles incoming API requests and serving the Medusa Admin dashboard.
+ - The worker mode handles background tasks, such as scheduled jobs and subscribers.
+
+So, when choosing a hosting provider, make sure it supports deploying these resources. Also, for optimal experience, the hosting provider and plan must offer at least 2GB of RAM.
+
+***
+
+## 1. Configure Medusa Application
+
+### Worker Mode
+
+The `workerMode` configuration determines which mode the Medusa application runs in. When you deploy the Medusa application, you deploy two instances: one in server mode, and one in worker mode.
+
+Learn more about worker mode in the [Worker Module chapter](https://docs.medusajs.com/learn/production/worker-mode/index.html.md).
+
+So, add the following configuration in `medusa-config.ts`:
+
+```ts title="medusa-config.ts"
+module.exports = defineConfig({
+ projectConfig: {
+ // ...
+ workerMode: process.env.MEDUSA_WORKER_MODE as "shared" | "worker" | "server",
+ },
+})
+```
+
+Later, you’ll set different values of the `MEDUSA_WORKER_MODE` environment variable for each Medusa application deployment or instance.
+
+### Configure Medusa Admin
+
+The Medusa Admin is served by the Medusa server application. So, you need to disable it in the worker Medusa application only.
+
+To disable the Medusa Admin in the worker Medusa application while keeping it enabled in the server Medusa application, add the following configuration in `medusa-config.ts`:
+
+```ts title="medusa-config.ts"
+module.exports = defineConfig({
+ // ...
+ admin: {
+ disable: process.env.DISABLE_MEDUSA_ADMIN === "true",
+ },
+})
+```
+
+Later, you’ll set different values of the `DISABLE_MEDUSA_ADMIN` environment variable for each Medusa application instance.
+
+### Configure Redis URL
+
+The `redisUrl` configuration specifies the connection URL to Redis to store the Medusa server's session.
+
+Learn more in the [Medusa Configuration documentation](https://docs.medusajs.com/learn/configurations/medusa-config#redisurl/index.html.md).
+
+So, add the following configuration in `medusa-config.ts` :
+
+```ts title="medusa-config.ts"
+module.exports = defineConfig({
+ projectConfig: {
+ // ...
+ redisUrl: process.env.REDIS_URL,
+ },
+})
+```
+
+***
+
+## 2. Add predeploy Script
+
+Before you start the Medusa application in production, you should always run migrations and sync links.
+
+So, add the following script in `package.json`:
+
+```json
+"scripts": {
+ // ...
+ "predeploy": "medusa db:migrate"
+},
+```
+
+***
+
+## 3. Install Production Modules and Providers
+
+By default, your Medusa application uses modules and providers useful for development, such as the In-Memory Cache Module or the Local File Module Provider.
+
+It’s highly recommended to instead use modules and providers suitable for production, including:
+
+- [Redis Cache Module](https://docs.medusajs.com/resources/infrastructure-modules/cache/redis/index.html.md)
+- [Redis Event Bus Module](https://docs.medusajs.com/resources/infrastructure-modules/event/redis/index.html.md)
+- [Workflow Engine Redis Module](https://docs.medusajs.com/resources/infrastructure-modules/workflow-engine/redis/index.html.md)
+- [S3 File Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/file/s3/index.html.md) (or other file module providers production-ready).
+- [SendGrid Notification Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/notification/sendgrid/index.html.md) (or other notification module providers production-ready).
+
+Then, add these modules in `medusa-config.ts`:
+
+```ts title="medusa-config.ts"
+import { Modules } from "@medusajs/framework/utils"
+
+module.exports = defineConfig({
+ // ...
+ modules: [
+ {
+ resolve: "@medusajs/medusa/cache-redis",
+ options: {
+ redisUrl: process.env.REDIS_URL,
+ },
+ },
+ {
+ resolve: "@medusajs/medusa/event-bus-redis",
+ options: {
+ redisUrl: process.env.REDIS_URL,
+ },
+ },
+ {
+ resolve: "@medusajs/medusa/workflow-engine-redis",
+ options: {
+ redis: {
+ url: process.env.REDIS_URL,
+ },
+ },
+ },
+ ],
+})
+```
+
+Check out the [Integrations](https://docs.medusajs.com/resources/integrations/index.html.md) and [Infrastructure Modules](https://docs.medusajs.com/resources/infrastructure-modules/index.html.md) documentation for other modules and providers to use.
+
+***
+
+## 4. Create PostgreSQL and Redis Databases
+
+Your Medusa application must connect to PostgreSQL and Redis databases. So, before you deploy it, create production PostgreSQL and Redis databases.
+
+If your hosting provider doesn't support databases, you can use [Neon for PostgreSQL database hosting](https://neon.tech/), and [Redis Cloud for the Redis database hosting](https://redis.io/cloud/).
+
+After hosting both databases, keep their connection URLs for the next steps.
+
+***
+
+## 5. Deploy Medusa Application in Server Mode
+
+As mentioned earlier, you'll deploy two instances or create two deployments of your Medusa application: one in server mode, and the other in worker mode.
+
+The deployment steps depend on your hosting provider. This section provides the general steps to perform during the deployment.
+
+### Set Environment Variables
+
+When setting the environment variables of the Medusa application, set the following variables:
+
+```bash
+COOKIE_SECRET=supersecret # TODO GENERATE SECURE SECRET
+JWT_SECRET=supersecret # TODO GENERATE SECURE SECRET
+STORE_CORS= # STOREFRONT URL
+ADMIN_CORS= # ADMIN URL
+AUTH_CORS= # STOREFRONT AND ADMIN URLS, SEPARATED BY COMMAS
+DISABLE_MEDUSA_ADMIN=false
+MEDUSA_WORKER_MODE=server
+PORT=9000
+DATABASE_URL= # POSTGRES DATABASE URL
+REDIS_URL= # REDIS DATABASE URL
+```
+
+Where:
+
+- The value of `COOKIE_SECRET` and `JWT_SECRET` must be a randomly generated secret.
+- `STORE_CORS`'s value is the URL of your storefront. If you don’t have it yet, you can skip adding it for now.
+- `ADMIN_CORS`'s value is the URL of the admin dashboard, which is the same as the server Medusa application. You can add it later if you don't currently have it.
+- `AUTH_CORS`'s value is the URLs of any application authenticating users, customers, or other actor types, such as the storefront and admin URLs. The URLs are separated by commas. If you don’t have the URLs yet, you can set its value later.
+- Set `DISABLE_MEDUSA_ADMIN`'s value to `false` so that the admin is built with the server application.
+- Set the PostgreSQL database's connection URL as the value of `DATABASE_URL`
+- Set the Redis database's connection URL as the value of `REDIS_URL`.
+
+Feel free to add any other relevant environment variables, such as for integrations and Infrastructure Modules. If you're using environment variables in your admin customizations, make sure to set them as well, as they're inlined during the build process.
+
+### Set Start Command
+
+The Medusa application's production build, which is created using the `build` command, outputs the Medusa application to `.medusa/server`. So, you must install the dependencies in the `.medusa/server` directory, then run the `start` command in it.
+
+If your hosting provider doesn't support setting a current-working directory, set the start command to the following:
+
+```bash npm2yarn
+cd .medusa/server && npm install && npm run predeploy && npm run start
+```
+
+Notice that you run the `predeploy` command before starting the Medusa application to run migrations and sync links whenever there's an update.
+
+### Set Backend URL in Admin Configuration
+
+The Medusa Admin is built and hosted statically. To send requests to the Medusa server application, you must set the backend URL in the Medusa Admin's configuration.
+
+After you’ve obtained the Medusa application’s URL, add the following configuration to `medusa-config.ts`:
+
+```ts title="medusa-config.ts"
+module.exports = defineConfig({
+ // ...
+ admin: {
+ // ...
+ backendUrl: process.env.MEDUSA_BACKEND_URL,
+ },
+})
+```
+
+Then, push the changes to the GitHub repository or deployed application.
+
+In your hosting provider, add or modify the following environment variables for the Medusa application in server mode:
+
+```bash
+ADMIN_CORS= # MEDUSA APPLICATION URL
+AUTH_CORS= # ADD MEDUSA APPLICATION URL
+MEDUSA_BACKEND_URL= # URL TO DEPLOYED MEDUSA APPLICATION
+```
+
+Where you set the value of `ADMIN_CORS` and `MEDUSA_BACKEND_URL` to the Medusa application’s URL, and you add the URL to `AUTH_CORS`.
+
+After setting the environment variables, make sure to restart the deployment for the changes to take effect.
+
+Remember to separate URLs in `AUTH_CORS` by commas.
+
+***
+
+## 6. Deploy Medusa Application in Worker Mode
+
+Next, you'll deploy the Medusa application in worker mode.
+
+As explained in the previous section, the deployment steps depend on your hosting provider. This section provides the general steps to perform during the deployment.
+
+### Set Environment Variables
+
+When setting the environment variables of the Medusa application, set the following variables:
+
+```bash
+COOKIE_SECRET=supersecret # TODO GENERATE SECURE SECRET
+JWT_SECRET=supersecret # TODO GENERATE SECURE SECRET
+DISABLE_MEDUSA_ADMIN=true
+MEDUSA_WORKER_MODE=worker
+PORT=9000
+DATABASE_URL= # POSTGRES DATABASE URL
+REDIS_URL= # REDIS DATABASE URL
+```
+
+Where:
+
+- The value of `COOKIE_SECRET` and `JWT_SECRET` must be a randomly generated secret.
+- Set `DISABLE_MEDUSA_ADMIN`'s value to `true` so that the admin isn't built with the worker application.
+- Set the PostgreSQL database's connection URL as the value of `DATABASE_URL`
+- Set the Redis database's connection URL as the value of `REDIS_URL`.
+
+Feel free to add any other relevant environment variables, such as for integrations and Infrastructure Modules.
+
+### Set Start Command
+
+The Medusa application's production build, which is created using the `build` command, outputs the Medusa application to `.medusa/server`. So, you must install the dependencies in the `.medusa/server` directory, then run the `start` command in it.
+
+If your hosting provider doesn't support setting a current-working directory, set the start command to the following:
+
+```bash npm2yarn
+cd .medusa/server && npm install && npm run start
+```
+
+***
+
+## 7. Test Deployed Application
+
+Once the application is deployed and live, go to `/health`, where `` is the URL of the Medusa application in server mode. If the deployment was successful, you’ll see the `OK` response.
+
+The Medusa Admin is also available at `/app`.
+
+***
+
+## Create Admin User
+
+If your hosting provider supports running commands in your Medusa application's directory, run the following command to create an admin user:
+
+```bash
+npx medusa user -e admin-medusa@test.com -p supersecret
+```
+
+Replace the email `admin-medusa@test.com` and password `supersecret` with the credentials you want.
+
+You can use these credentials to log into the Medusa Admin dashboard.
+
+
+# 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.
+
+
+
+***
+
+## 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.
@@ -4944,207 +4944,6 @@ Now that you have brands in your Medusa application, you want to associate a bra
In the next chapters, you'll learn how to build associations between data models defined in different modules.
-# Write Tests for Modules
-
-In this chapter, you'll learn about `moduleIntegrationTestRunner` from Medusa's Testing Framework and how to use it to write integration tests for a module's main service.
-
-### 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).
-
-
-# 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: Implement Brand Module
In this chapter, you'll build a Brand Module that adds a `brand` table to the database and provides data-management features for it.
@@ -5967,76 +5766,6 @@ The [Admin Components guides](https://docs.medusajs.com/resources/admin-componen
In the next chapter, you'll add a UI route that displays the list of brands in your application and allows admin users.
-# Guide: Define Module Link Between Brand and Product
-
-In this chapter, you'll learn how to define a module link between a brand defined in the [custom Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md), and a product defined in the [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md) that's available in your Medusa application out-of-the-box.
-
-Modules are [isolated](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md) from other resources, ensuring that they're integrated into the Medusa application without side effects. However, you may need to associate data models of different modules, or you're trying to extend data models from Commerce Modules with custom properties. To do that, you define module links.
-
-A module link forms an association between two data models of different modules while maintaining module isolation. You can then manage and query linked records of the data models using Medusa's Modules SDK.
-
-In this chapter, you'll define a module link between the `Brand` data model of the Brand Module, and the `Product` data model of the Product Module. In later chapters, you'll manage and retrieve linked product and brand records.
-
-Learn more about module links in [this chapters](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md).
-
-### Prerequisites
-
-- [Brand Module having a Brand data model](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md)
-
-## 1. Define Link
-
-Links are defined in a TypeScript or JavaScript file under the `src/links` directory. The file defines and exports the link using `defineLink` from the Modules SDK.
-
-So, to define a link between the `Product` and `Brand` models, create the file `src/links/product-brand.ts` with the following content:
-
-
-
-```ts title="src/links/product-brand.ts" highlights={highlights}
-import BrandModule from "../modules/brand"
-import ProductModule from "@medusajs/medusa/product"
-import { defineLink } from "@medusajs/framework/utils"
-
-export default defineLink(
- {
- linkable: ProductModule.linkable.product,
- isList: true,
- },
- BrandModule.linkable.brand
-)
-```
-
-You import each module's definition object from the `index.ts` file of the module's directory. Each module object has a special `linkable` property that holds the data models' link configurations.
-
-The `defineLink` function accepts two parameters of the same type, which is either:
-
-- The data model's link configuration, which you access from the Module's `linkable` property;
-- Or an object that has two properties:
- - `linkable`: the data model's link configuration, which you access from the Module's `linkable` property.
- - `isList`: A boolean indicating whether many records of the data model can be linked to the other model.
-
-So, in the above code snippet, you define a link between the `Product` and `Brand` data models. Since a brand can be associated with multiple products, you enable `isList` in the `Product` model's object.
-
-***
-
-## 2. Sync the Link to the Database
-
-A module link is represented in the database as a table that stores the IDs of linked records. So, after defining the link, run the following command to create the module link's table in the database:
-
-```bash
-npx medusa db:migrate
-```
-
-This command reflects migrations on the database and syncs module links, which creates a table for the `product-brand` link.
-
-You can also run the `npx medusa db:sync-links` to just sync module links without running migrations.
-
-***
-
-## Next Steps: Extend Create Product Flow
-
-In the next chapter, you'll extend Medusa's workflow and API route that create a product to allow associating a brand with a product. You'll also learn how to link brand and product records.
-
-
# Guide: Extend Create Product Flow
After linking the [custom Brand data model](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) and Medusa's [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md) in the [previous chapter](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md), you'll extend the create product workflow and API route to allow associating a brand with a product.
@@ -6397,276 +6126,233 @@ Clients, such as the Medusa Admin dashboard, can now use brand-related features,
In the next chapters, you'll learn how to customize the Medusa Admin to show a product's brand on its details page, and to show a new page with the list of brands in your store.
-# Guide: Sync Brands from Medusa to Third-Party
+# Guide: Define Module Link Between Brand and Product
-In the [previous chapter](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md), you created a CMS Module that integrates a dummy third-party system. You can now perform actions using that module within your custom flows.
+In this chapter, you'll learn how to define a module link between a brand defined in the [custom Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md), and a product defined in the [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md) that's available in your Medusa application out-of-the-box.
-In another previous chapter, you [added a workflow](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md) that creates a brand. After integrating the CMS, you want to sync that brand to the third-party system as well.
+Modules are [isolated](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md) from other resources, ensuring that they're integrated into the Medusa application without side effects. However, you may need to associate data models of different modules, or you're trying to extend data models from Commerce Modules with custom properties. To do that, you define module links.
-Medusa has an event system that emits events when an operation is performed. It allows you to listen to those events and perform an asynchronous action in a function called a [subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). This is useful to perform actions that aren't integral to the original flow, such as syncing data to a third-party system.
+A module link forms an association between two data models of different modules while maintaining module isolation. You can then manage and query linked records of the data models using Medusa's Modules SDK.
-Learn more about Medusa's event system and subscribers in [this chapter](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md).
+In this chapter, you'll define a module link between the `Brand` data model of the Brand Module, and the `Product` data model of the Product Module. In later chapters, you'll manage and retrieve linked product and brand records.
-In this chapter, you'll modify the `createBrandWorkflow` you created before to emit a custom event that indicates a brand was created. Then, you'll listen to that event in a subscriber to sync the brand to the third-party CMS. You'll implement the sync logic within a workflow that you execute in the subscriber.
+Learn more about module links in [this chapters](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md).
### Prerequisites
-- [createBrandWorkflow](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md)
-- [CMS Module](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md)
+- [Brand Module having a Brand data model](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md)
-## 1. Emit Event in createBrandWorkflow
+## 1. Define Link
-Since syncing the brand to the third-party system isn't integral to creating a brand, you'll emit a custom event indicating that a brand was created.
+Links are defined in a TypeScript or JavaScript file under the `src/links` directory. The file defines and exports the link using `defineLink` from the Modules SDK.
-Medusa provides an `emitEventStep` that allows you to emit an event in your workflows. So, in the `createBrandWorkflow` defined in `src/workflows/create-brand.ts`, use the `emitEventStep` helper step after the `createBrandStep`:
+So, to define a link between the `Product` and `Brand` models, create the file `src/links/product-brand.ts` with the following content:
-```ts title="src/workflows/create-brand.ts" highlights={eventHighlights}
-// other imports...
-import {
- emitEventStep,
-} from "@medusajs/medusa/core-flows"
+
-// ...
+```ts title="src/links/product-brand.ts" highlights={highlights}
+import BrandModule from "../modules/brand"
+import ProductModule from "@medusajs/medusa/product"
+import { defineLink } from "@medusajs/framework/utils"
-export const createBrandWorkflow = createWorkflow(
- "create-brand",
- (input: CreateBrandInput) => {
- // ...
-
- emitEventStep({
- eventName: "brand.created",
- data: {
- id: brand.id,
- },
- })
-
- return new WorkflowResponse(brand)
- }
-)
-```
-
-The `emitEventStep` accepts an object parameter having two properties:
-
-- `eventName`: The name of the event to emit. You'll use this name later to listen to the event in a subscriber.
-- `data`: The data payload to emit with the event. This data is passed to subscribers that listen to the event. You add the brand's ID to the data payload, informing the subscribers which brand was created.
-
-You'll learn how to handle this event in a later step.
-
-***
-
-## 2. Create Sync to Third-Party System Workflow
-
-The subscriber that will listen to the `brand.created` event will sync the created brand to the third-party CMS. So, you'll implement the syncing logic in a workflow, then execute the workflow in the subscriber.
-
-Workflows have a built-in durable execution engine that helps you complete tasks spanning multiple systems. Also, their rollback mechanism ensures that data is consistent across systems even when errors occur during execution.
-
-Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md).
-
-You'll create a `syncBrandToSystemWorkflow` that has two steps:
-
-- `useQueryGraphStep`: a step that Medusa provides to retrieve data using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). You'll use this to retrieve the brand's details using its ID.
-- `syncBrandToCmsStep`: a step that you'll create to sync the brand to the CMS.
-
-### syncBrandToCmsStep
-
-To implement the step that syncs the brand to the CMS, create the file `src/workflows/sync-brands-to-cms.ts` with the following content:
-
-
-
-```ts title="src/workflows/sync-brands-to-cms.ts" highlights={syncStepHighlights} collapsibleLines="1-6" expandButtonLabel="Show Imports"
-import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
-import { InferTypeOf } from "@medusajs/framework/types"
-import { Brand } from "../modules/brand/models/brand"
-import { CMS_MODULE } from "../modules/cms"
-import CmsModuleService from "../modules/cms/service"
-
-type SyncBrandToCmsStepInput = {
- brand: InferTypeOf
-}
-
-const syncBrandToCmsStep = createStep(
- "sync-brand-to-cms",
- async ({ brand }: SyncBrandToCmsStepInput, { container }) => {
- const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE)
-
- await cmsModuleService.createBrand(brand)
-
- return new StepResponse(null, brand.id)
+export default defineLink(
+ {
+ linkable: ProductModule.linkable.product,
+ isList: true,
},
- async (id, { container }) => {
- if (!id) {
- return
- }
-
- const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE)
-
- await cmsModuleService.deleteBrand(id)
- }
+ BrandModule.linkable.brand
)
```
-You create the `syncBrandToCmsStep` that accepts a brand as an input. In the step, you resolve the CMS Module's service from the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) and use its `createBrand` method. This method will create the brand in the third-party CMS.
+You import each module's definition object from the `index.ts` file of the module's directory. Each module object has a special `linkable` property that holds the data models' link configurations.
-You also pass the brand's ID to the step's compensation function. In this function, you delete the brand in the third-party CMS if an error occurs during the workflow's execution.
+The `defineLink` function accepts two parameters of the same type, which is either:
-Learn more about compensation functions in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md).
+- The data model's link configuration, which you access from the Module's `linkable` property;
+- Or an object that has two properties:
+ - `linkable`: the data model's link configuration, which you access from the Module's `linkable` property.
+ - `isList`: A boolean indicating whether many records of the data model can be linked to the other model.
-### Create Workflow
+So, in the above code snippet, you define a link between the `Product` and `Brand` data models. Since a brand can be associated with multiple products, you enable `isList` in the `Product` model's object.
-You can now create the workflow that uses the above step. Add the workflow to the same `src/workflows/sync-brands-to-cms.ts` file:
+***
-```ts title="src/workflows/sync-brands-to-cms.ts" highlights={syncWorkflowHighlights}
-// other imports...
-import {
+## 2. Sync the Link to the Database
+
+A module link is represented in the database as a table that stores the IDs of linked records. So, after defining the link, run the following command to create the module link's table in the database:
+
+```bash
+npx medusa db:migrate
+```
+
+This command reflects migrations on the database and syncs module links, which creates a table for the `product-brand` link.
+
+You can also run the `npx medusa db:sync-links` to just sync module links without running migrations.
+
+***
+
+## Next Steps: Extend Create Product Flow
+
+In the next chapter, you'll extend Medusa's workflow and API route that create a product to allow associating a brand with a product. You'll also learn how to link brand and product records.
+
+
+# Guide: Integrate Third-Party Brand System
+
+In the previous chapters, you've created a [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) that adds brands to your application. In this chapter, you'll integrate a dummy Content-Management System (CMS) in a new module. The module's service will provide methods to retrieve and manage brands in the CMS. You'll later use this service to sync data from and to the CMS.
+
+Learn more about modules in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md).
+
+## 1. Create Module Directory
+
+You'll integrate the third-party system in a new CMS Module. So, create the directory `src/modules/cms` that will hold the module's resources.
+
+
+
+***
+
+## 2. Create Module Service
+
+Next, you'll create the module's service. It will provide methods to connect and perform actions with the third-party system.
+
+Create the CMS Module's service at `src/modules/cms/service.ts` with the following content:
+
+
+
+```ts title="src/modules/cms/service.ts" highlights={serviceHighlights}
+import { Logger, ConfigModule } from "@medusajs/framework/types"
+
+export type ModuleOptions = {
+ apiKey: string
+}
+
+type InjectedDependencies = {
+ logger: Logger
+ configModule: ConfigModule
+}
+
+class CmsModuleService {
+ private options_: ModuleOptions
+ private logger_: Logger
+
+ constructor({ logger }: InjectedDependencies, options: ModuleOptions) {
+ this.logger_ = logger
+ this.options_ = options
+
+ // TODO initialize SDK
+ }
+}
+
+export default CmsModuleService
+```
+
+You create a `CmsModuleService` that will hold the methods to connect to the third-party CMS. A service's constructor accepts two parameters:
+
+1. The module's container. Since a module is [isolated](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md), it has a [local container](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md) different than the Medusa container you use in other customizations. This container holds Framework tools like the [Logger utility](https://docs.medusajs.com/learn/debugging-and-testing/logging/index.html.md) and resources within the module.
+2. Options passed to the module when it's later added in Medusa's configurations. These options are useful to pass secret keys or configurations that ensure your module is re-usable across applications. For the CMS Module, you accept the API key to connect to the dummy CMS as an option.
+
+When integrating a third-party system that has a Node.js SDK or client, you can initialize that client in the constructor to be used in the service's methods.
+
+### Integration Methods
+
+Next, you'll add methods that simulate sending requests to a third-party CMS. You'll use these methods later to sync brands from and to the CMS.
+
+Add the following methods in the `CmsModuleService`:
+
+```ts title="src/modules/cms/service.ts" highlights={methodsHighlights}
+export class CmsModuleService {
// ...
- createWorkflow,
- WorkflowResponse,
-} from "@medusajs/framework/workflows-sdk"
-import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
-// ...
-
-type SyncBrandToCmsWorkflowInput = {
- id: string
-}
-
-export const syncBrandToCmsWorkflow = createWorkflow(
- "sync-brand-to-cms",
- (input: SyncBrandToCmsWorkflowInput) => {
- // @ts-ignore
- const { data: brands } = useQueryGraphStep({
- entity: "brand",
- fields: ["*"],
- filters: {
- id: input.id,
- },
- options: {
- throwIfKeyNotFound: true,
- },
- })
-
- syncBrandToCmsStep({
- brand: brands[0],
- } as SyncBrandToCmsStepInput)
-
- return new WorkflowResponse({})
+ // a dummy method to simulate sending a request,
+ // in a realistic scenario, you'd use an SDK, fetch, or axios clients
+ private async sendRequest(url: string, method: string, data?: any) {
+ this.logger_.info(`Sending a ${method} request to ${url}.`)
+ this.logger_.info(`Request Data: ${JSON.stringify(data, null, 2)}`)
+ this.logger_.info(`API Key: ${JSON.stringify(this.options_.apiKey, null, 2)}`)
}
-)
-```
-You create a `syncBrandToCmsWorkflow` that accepts the brand's ID as input. The workflow has the following steps:
+ async createBrand(brand: Record) {
+ await this.sendRequest("/brands", "POST", brand)
+ }
-- `useQueryGraphStep`: Retrieve the brand's details using Query. You pass the brand's ID as a filter, and set the `throwIfKeyNotFound` option to true so that the step throws an error if a brand with the specified ID doesn't exist.
-- `syncBrandToCmsStep`: Create the brand in the third-party CMS.
+ async deleteBrand(id: string) {
+ await this.sendRequest(`/brands/${id}`, "DELETE")
+ }
-You'll execute this workflow in the subscriber next.
+ async retrieveBrands(): Promise[]> {
+ await this.sendRequest("/brands", "GET")
-Learn more about `useQueryGraphStep` in [this reference](https://docs.medusajs.com/resources/references/helper-steps/useQueryGraphStep/index.html.md).
-
-***
-
-## 3. Handle brand.created Event
-
-You now have a workflow with the logic to sync a brand to the CMS. You need to execute this workflow whenever the `brand.created` event is emitted. So, you'll create a subscriber that listens to and handle the event.
-
-Subscribers are created in a TypeScript or JavaScript file under the `src/subscribers` directory. So, create the file `src/subscribers/brand-created.ts` with the following content:
-
-
-
-```ts title="src/subscribers/brand-created.ts" highlights={subscriberHighlights}
-import type {
- SubscriberConfig,
- SubscriberArgs,
-} from "@medusajs/framework"
-import { syncBrandToCmsWorkflow } from "../workflows/sync-brands-to-cms"
-
-export default async function brandCreatedHandler({
- event: { data },
- container,
-}: SubscriberArgs<{ id: string }>) {
- await syncBrandToCmsWorkflow(container).run({
- input: data,
- })
-}
-
-export const config: SubscriberConfig = {
- event: "brand.created",
+ return []
+ }
}
```
-A subscriber file must export:
+The `sendRequest` method sends requests to the third-party CMS. Since this guide isn't using a real CMS, it only simulates the sending by logging messages in the terminal.
-- The asynchronous function that's executed when the event is emitted. This must be the file's default export.
-- An object that holds the subscriber's configurations. It has an `event` property that indicates the name of the event that the subscriber is listening to.
+You also add three methods that use the `sendRequest` method:
-The subscriber function accepts an object parameter that has two properties:
-
-- `event`: An object of event details. Its `data` property holds the event's data payload, which is the brand's ID.
-- `container`: The Medusa container used to resolve Framework and commerce tools.
-
-In the function, you execute the `syncBrandToCmsWorkflow`, passing it the data payload as an input. So, everytime a brand is created, Medusa will execute this function, which in turn executes the workflow to sync the brand to the CMS.
-
-Learn more about subscribers in [this chapter](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md).
+- `createBrand` that creates a brand in the third-party system.
+- `deleteBrand` that deletes the brand in the third-party system.
+- `retrieveBrands` to retrieve a brand from the third-party system.
***
-## Test it Out
+## 3. Export Module Definition
-To test the subscriber and workflow out, you'll use the [Create Brand API route](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md) you created in a previous chapter.
+After creating the module's service, you'll export the module definition indicating the module's name and service.
-First, start the Medusa application:
+Create the file `src/modules/cms/index.ts` with the following content:
-```bash npm2yarn
-npm run dev
+
+
+```ts title="src/modules/cms/index.ts"
+import { Module } from "@medusajs/framework/utils"
+import CmsModuleService from "./service"
+
+export const CMS_MODULE = "cms"
+
+export default Module(CMS_MODULE, {
+ service: CmsModuleService,
+})
```
-Since the `/admin/brands` API route has a `/admin` prefix, it's only accessible by authenticated admin users. So, to retrieve an authenticated token of your admin user, send a `POST` request to the `/auth/user/emailpass` API Route:
+You use `Module` from the Modules SDK to export the module's defintion, indicating that the module's name is `cms` and its service is `CmsModuleService`.
+
+***
+
+## 4. Add Module to Medusa's Configurations
+
+Finally, add the module to the Medusa configurations at `medusa-config.ts`:
+
+```ts title="medusa-config.ts"
+module.exports = defineConfig({
+ // ...
+ modules: [
+ // ...
+ {
+ resolve: "./src/modules/cms",
+ options: {
+ apiKey: process.env.CMS_API_KEY,
+ },
+ },
+ ],
+})
+```
+
+The object passed in `modules` accept an `options` property, whose value is an object of options to pass to the module. These are the options you receive in the `CmsModuleService`'s constructor.
+
+You can add the `CMS_API_KEY` environment variable to `.env`:
```bash
-curl -X POST 'http://localhost:9000/auth/user/emailpass' \
--H 'Content-Type: application/json' \
---data-raw '{
- "email": "admin@medusa-test.com",
- "password": "supersecret"
-}'
-```
-
-Make sure to replace the email and password with your admin user's credentials.
-
-Don't have an admin user? Refer to [this guide](https://docs.medusajs.com/learn/installation#create-medusa-admin-user/index.html.md).
-
-Then, send a `POST` request to `/admin/brands`, passing the token received from the previous request in the `Authorization` header:
-
-```bash
-curl -X POST 'http://localhost:9000/admin/brands' \
--H 'Content-Type: application/json' \
--H 'Authorization: Bearer {token}' \
---data '{
- "name": "Acme"
-}'
-```
-
-This request returns the created brand. If you check the logs, you'll find the `brand.created` event was emitted, and that the request to the third-party system was simulated:
-
-```plain
-info: Processing brand.created which has 1 subscribers
-http: POST /admin/brands ← - (200) - 16.418 ms
-info: Sending a POST request to /brands.
-info: Request Data: {
- "id": "01JEDWENYD361P664WRQPMC3J8",
- "name": "Acme",
- "created_at": "2024-12-06T11:42:32.909Z",
- "updated_at": "2024-12-06T11:42:32.909Z",
- "deleted_at": null
-}
-info: API Key: "123"
+CMS_API_KEY=123
```
***
-## Next Chapter: Sync Brand from Third-Party CMS to Medusa
+## Next Steps: Sync Brand From Medusa to CMS
-You can also automate syncing data from a third-party system to Medusa at a regular interval. In the next chapter, you'll learn how to sync brands from the third-party CMS to Medusa once a day.
+You can now use the CMS Module's service to perform actions on the third-party CMS.
+
+In the next chapter, you'll learn how to emit an event when a brand is created, then handle that event to sync the brand from Medusa to the third-party service.
# Guide: Schedule Syncing Brands from Third-Party
@@ -6978,6 +6664,278 @@ By following the previous chapters, you utilized the Medusa Framework and orches
With Medusa, you can integrate any service from your commerce ecosystem with ease. You don't have to set up separate applications to manage your different customizations, or worry about data inconsistency across systems. Your efforts only go into implementing the business logic that ties your systems together.
+# Guide: Sync Brands from Medusa to Third-Party
+
+In the [previous chapter](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md), you created a CMS Module that integrates a dummy third-party system. You can now perform actions using that module within your custom flows.
+
+In another previous chapter, you [added a workflow](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md) that creates a brand. After integrating the CMS, you want to sync that brand to the third-party system as well.
+
+Medusa has an event system that emits events when an operation is performed. It allows you to listen to those events and perform an asynchronous action in a function called a [subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). This is useful to perform actions that aren't integral to the original flow, such as syncing data to a third-party system.
+
+Learn more about Medusa's event system and subscribers in [this chapter](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md).
+
+In this chapter, you'll modify the `createBrandWorkflow` you created before to emit a custom event that indicates a brand was created. Then, you'll listen to that event in a subscriber to sync the brand to the third-party CMS. You'll implement the sync logic within a workflow that you execute in the subscriber.
+
+### Prerequisites
+
+- [createBrandWorkflow](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md)
+- [CMS Module](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md)
+
+## 1. Emit Event in createBrandWorkflow
+
+Since syncing the brand to the third-party system isn't integral to creating a brand, you'll emit a custom event indicating that a brand was created.
+
+Medusa provides an `emitEventStep` that allows you to emit an event in your workflows. So, in the `createBrandWorkflow` defined in `src/workflows/create-brand.ts`, use the `emitEventStep` helper step after the `createBrandStep`:
+
+```ts title="src/workflows/create-brand.ts" highlights={eventHighlights}
+// other imports...
+import {
+ emitEventStep,
+} from "@medusajs/medusa/core-flows"
+
+// ...
+
+export const createBrandWorkflow = createWorkflow(
+ "create-brand",
+ (input: CreateBrandInput) => {
+ // ...
+
+ emitEventStep({
+ eventName: "brand.created",
+ data: {
+ id: brand.id,
+ },
+ })
+
+ return new WorkflowResponse(brand)
+ }
+)
+```
+
+The `emitEventStep` accepts an object parameter having two properties:
+
+- `eventName`: The name of the event to emit. You'll use this name later to listen to the event in a subscriber.
+- `data`: The data payload to emit with the event. This data is passed to subscribers that listen to the event. You add the brand's ID to the data payload, informing the subscribers which brand was created.
+
+You'll learn how to handle this event in a later step.
+
+***
+
+## 2. Create Sync to Third-Party System Workflow
+
+The subscriber that will listen to the `brand.created` event will sync the created brand to the third-party CMS. So, you'll implement the syncing logic in a workflow, then execute the workflow in the subscriber.
+
+Workflows have a built-in durable execution engine that helps you complete tasks spanning multiple systems. Also, their rollback mechanism ensures that data is consistent across systems even when errors occur during execution.
+
+Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md).
+
+You'll create a `syncBrandToSystemWorkflow` that has two steps:
+
+- `useQueryGraphStep`: a step that Medusa provides to retrieve data using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). You'll use this to retrieve the brand's details using its ID.
+- `syncBrandToCmsStep`: a step that you'll create to sync the brand to the CMS.
+
+### syncBrandToCmsStep
+
+To implement the step that syncs the brand to the CMS, create the file `src/workflows/sync-brands-to-cms.ts` with the following content:
+
+
+
+```ts title="src/workflows/sync-brands-to-cms.ts" highlights={syncStepHighlights} collapsibleLines="1-6" expandButtonLabel="Show Imports"
+import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
+import { InferTypeOf } from "@medusajs/framework/types"
+import { Brand } from "../modules/brand/models/brand"
+import { CMS_MODULE } from "../modules/cms"
+import CmsModuleService from "../modules/cms/service"
+
+type SyncBrandToCmsStepInput = {
+ brand: InferTypeOf
+}
+
+const syncBrandToCmsStep = createStep(
+ "sync-brand-to-cms",
+ async ({ brand }: SyncBrandToCmsStepInput, { container }) => {
+ const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE)
+
+ await cmsModuleService.createBrand(brand)
+
+ return new StepResponse(null, brand.id)
+ },
+ async (id, { container }) => {
+ if (!id) {
+ return
+ }
+
+ const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE)
+
+ await cmsModuleService.deleteBrand(id)
+ }
+)
+```
+
+You create the `syncBrandToCmsStep` that accepts a brand as an input. In the step, you resolve the CMS Module's service from the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) and use its `createBrand` method. This method will create the brand in the third-party CMS.
+
+You also pass the brand's ID to the step's compensation function. In this function, you delete the brand in the third-party CMS if an error occurs during the workflow's execution.
+
+Learn more about compensation functions in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md).
+
+### Create Workflow
+
+You can now create the workflow that uses the above step. Add the workflow to the same `src/workflows/sync-brands-to-cms.ts` file:
+
+```ts title="src/workflows/sync-brands-to-cms.ts" highlights={syncWorkflowHighlights}
+// other imports...
+import {
+ // ...
+ createWorkflow,
+ WorkflowResponse,
+} from "@medusajs/framework/workflows-sdk"
+import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
+
+// ...
+
+type SyncBrandToCmsWorkflowInput = {
+ id: string
+}
+
+export const syncBrandToCmsWorkflow = createWorkflow(
+ "sync-brand-to-cms",
+ (input: SyncBrandToCmsWorkflowInput) => {
+ // @ts-ignore
+ const { data: brands } = useQueryGraphStep({
+ entity: "brand",
+ fields: ["*"],
+ filters: {
+ id: input.id,
+ },
+ options: {
+ throwIfKeyNotFound: true,
+ },
+ })
+
+ syncBrandToCmsStep({
+ brand: brands[0],
+ } as SyncBrandToCmsStepInput)
+
+ return new WorkflowResponse({})
+ }
+)
+```
+
+You create a `syncBrandToCmsWorkflow` that accepts the brand's ID as input. The workflow has the following steps:
+
+- `useQueryGraphStep`: Retrieve the brand's details using Query. You pass the brand's ID as a filter, and set the `throwIfKeyNotFound` option to true so that the step throws an error if a brand with the specified ID doesn't exist.
+- `syncBrandToCmsStep`: Create the brand in the third-party CMS.
+
+You'll execute this workflow in the subscriber next.
+
+Learn more about `useQueryGraphStep` in [this reference](https://docs.medusajs.com/resources/references/helper-steps/useQueryGraphStep/index.html.md).
+
+***
+
+## 3. Handle brand.created Event
+
+You now have a workflow with the logic to sync a brand to the CMS. You need to execute this workflow whenever the `brand.created` event is emitted. So, you'll create a subscriber that listens to and handle the event.
+
+Subscribers are created in a TypeScript or JavaScript file under the `src/subscribers` directory. So, create the file `src/subscribers/brand-created.ts` with the following content:
+
+
+
+```ts title="src/subscribers/brand-created.ts" highlights={subscriberHighlights}
+import type {
+ SubscriberConfig,
+ SubscriberArgs,
+} from "@medusajs/framework"
+import { syncBrandToCmsWorkflow } from "../workflows/sync-brands-to-cms"
+
+export default async function brandCreatedHandler({
+ event: { data },
+ container,
+}: SubscriberArgs<{ id: string }>) {
+ await syncBrandToCmsWorkflow(container).run({
+ input: data,
+ })
+}
+
+export const config: SubscriberConfig = {
+ event: "brand.created",
+}
+```
+
+A subscriber file must export:
+
+- The asynchronous function that's executed when the event is emitted. This must be the file's default export.
+- An object that holds the subscriber's configurations. It has an `event` property that indicates the name of the event that the subscriber is listening to.
+
+The subscriber function accepts an object parameter that has two properties:
+
+- `event`: An object of event details. Its `data` property holds the event's data payload, which is the brand's ID.
+- `container`: The Medusa container used to resolve Framework and commerce tools.
+
+In the function, you execute the `syncBrandToCmsWorkflow`, passing it the data payload as an input. So, everytime a brand is created, Medusa will execute this function, which in turn executes the workflow to sync the brand to the CMS.
+
+Learn more about subscribers in [this chapter](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md).
+
+***
+
+## Test it Out
+
+To test the subscriber and workflow out, you'll use the [Create Brand API route](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md) you created in a previous chapter.
+
+First, start the Medusa application:
+
+```bash npm2yarn
+npm run dev
+```
+
+Since the `/admin/brands` API route has a `/admin` prefix, it's only accessible by authenticated admin users. So, to retrieve an authenticated token of your admin user, send a `POST` request to the `/auth/user/emailpass` API Route:
+
+```bash
+curl -X POST 'http://localhost:9000/auth/user/emailpass' \
+-H 'Content-Type: application/json' \
+--data-raw '{
+ "email": "admin@medusa-test.com",
+ "password": "supersecret"
+}'
+```
+
+Make sure to replace the email and password with your admin user's credentials.
+
+Don't have an admin user? Refer to [this guide](https://docs.medusajs.com/learn/installation#create-medusa-admin-user/index.html.md).
+
+Then, send a `POST` request to `/admin/brands`, passing the token received from the previous request in the `Authorization` header:
+
+```bash
+curl -X POST 'http://localhost:9000/admin/brands' \
+-H 'Content-Type: application/json' \
+-H 'Authorization: Bearer {token}' \
+--data '{
+ "name": "Acme"
+}'
+```
+
+This request returns the created brand. If you check the logs, you'll find the `brand.created` event was emitted, and that the request to the third-party system was simulated:
+
+```plain
+info: Processing brand.created which has 1 subscribers
+http: POST /admin/brands ← - (200) - 16.418 ms
+info: Sending a POST request to /brands.
+info: Request Data: {
+ "id": "01JEDWENYD361P664WRQPMC3J8",
+ "name": "Acme",
+ "created_at": "2024-12-06T11:42:32.909Z",
+ "updated_at": "2024-12-06T11:42:32.909Z",
+ "deleted_at": null
+}
+info: API Key: "123"
+```
+
+***
+
+## Next Chapter: Sync Brand from Third-Party CMS to Medusa
+
+You can also automate syncing data from a third-party system to Medusa at a regular interval. In the next chapter, you'll learn how to sync brands from the third-party CMS to Medusa once a day.
+
+
# Admin Development Constraints
This chapter lists some constraints of admin widgets and UI routes.
@@ -7023,163 +6981,86 @@ export const config = defineWidgetConfig({
```
-# Guide: Integrate Third-Party Brand System
+# Write Integration Tests
-In the previous chapters, you've created a [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) that adds brands to your application. In this chapter, you'll integrate a dummy Content-Management System (CMS) in a new module. The module's service will provide methods to retrieve and manage brands in the CMS. You'll later use this service to sync data from and to the CMS.
+In this chapter, you'll learn about `medusaIntegrationTestRunner` from Medusa's Testing Framework and how to use it to write integration tests.
-Learn more about modules in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md).
+### Prerequisites
-## 1. Create Module Directory
+- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md)
-You'll integrate the third-party system in a new CMS Module. So, create the directory `src/modules/cms` that will hold the module's resources.
+## 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:
-## 2. Create Module Service
+```ts title="integration-tests/http/test.spec.ts" highlights={highlights}
+import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
-Next, you'll create the module's service. It will provide methods to connect and perform actions with the third-party system.
-
-Create the CMS Module's service at `src/modules/cms/service.ts` with the following content:
-
-
-
-```ts title="src/modules/cms/service.ts" highlights={serviceHighlights}
-import { Logger, ConfigModule } from "@medusajs/framework/types"
-
-export type ModuleOptions = {
- apiKey: string
-}
-
-type InjectedDependencies = {
- logger: Logger
- configModule: ConfigModule
-}
-
-class CmsModuleService {
- private options_: ModuleOptions
- private logger_: Logger
-
- constructor({ logger }: InjectedDependencies, options: ModuleOptions) {
- this.logger_ = logger
- this.options_ = options
-
- // TODO initialize SDK
- }
-}
-
-export default CmsModuleService
-```
-
-You create a `CmsModuleService` that will hold the methods to connect to the third-party CMS. A service's constructor accepts two parameters:
-
-1. The module's container. Since a module is [isolated](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md), it has a [local container](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md) different than the Medusa container you use in other customizations. This container holds Framework tools like the [Logger utility](https://docs.medusajs.com/learn/debugging-and-testing/logging/index.html.md) and resources within the module.
-2. Options passed to the module when it's later added in Medusa's configurations. These options are useful to pass secret keys or configurations that ensure your module is re-usable across applications. For the CMS Module, you accept the API key to connect to the dummy CMS as an option.
-
-When integrating a third-party system that has a Node.js SDK or client, you can initialize that client in the constructor to be used in the service's methods.
-
-### Integration Methods
-
-Next, you'll add methods that simulate sending requests to a third-party CMS. You'll use these methods later to sync brands from and to the CMS.
-
-Add the following methods in the `CmsModuleService`:
-
-```ts title="src/modules/cms/service.ts" highlights={methodsHighlights}
-export class CmsModuleService {
- // ...
-
- // a dummy method to simulate sending a request,
- // in a realistic scenario, you'd use an SDK, fetch, or axios clients
- private async sendRequest(url: string, method: string, data?: any) {
- this.logger_.info(`Sending a ${method} request to ${url}.`)
- this.logger_.info(`Request Data: ${JSON.stringify(data, null, 2)}`)
- this.logger_.info(`API Key: ${JSON.stringify(this.options_.apiKey, null, 2)}`)
- }
-
- async createBrand(brand: Record) {
- await this.sendRequest("/brands", "POST", brand)
- }
-
- async deleteBrand(id: string) {
- await this.sendRequest(`/brands/${id}`, "DELETE")
- }
-
- async retrieveBrands(): Promise[]> {
- await this.sendRequest("/brands", "GET")
-
- return []
- }
-}
-```
-
-The `sendRequest` method sends requests to the third-party CMS. Since this guide isn't using a real CMS, it only simulates the sending by logging messages in the terminal.
-
-You also add three methods that use the `sendRequest` method:
-
-- `createBrand` that creates a brand in the third-party system.
-- `deleteBrand` that deletes the brand in the third-party system.
-- `retrieveBrands` to retrieve a brand from the third-party system.
-
-***
-
-## 3. Export Module Definition
-
-After creating the module's service, you'll export the module definition indicating the module's name and service.
-
-Create the file `src/modules/cms/index.ts` with the following content:
-
-
-
-```ts title="src/modules/cms/index.ts"
-import { Module } from "@medusajs/framework/utils"
-import CmsModuleService from "./service"
-
-export const CMS_MODULE = "cms"
-
-export default Module(CMS_MODULE, {
- service: CmsModuleService,
+medusaIntegrationTestRunner({
+ testSuite: ({ api, getContainer }) => {
+ // TODO write tests...
+ },
})
+
+jest.setTimeout(60 * 1000)
```
-You use `Module` from the Modules SDK to export the module's defintion, indicating that the module's name is `cms` and its service is `CmsModuleService`.
+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:
-## 4. Add Module to Medusa's Configurations
+- `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.
-Finally, add the module to the Medusa configurations at `medusa-config.ts`:
+The tests in the `testSuite` function are written using [Jest](https://jestjs.io/).
-```ts title="medusa-config.ts"
-module.exports = defineConfig({
- // ...
- modules: [
- // ...
- {
- resolve: "./src/modules/cms",
- options: {
- apiKey: process.env.CMS_API_KEY,
- },
- },
- ],
-})
-```
+### Jest Timeout
-The object passed in `modules` accept an `options` property, whose value is an object of options to pass to the module. These are the options you receive in the `CmsModuleService`'s constructor.
+Since your tests connect to the database and perform actions that require more time than the typical tests, make sure to increase the timeout in your test:
-You can add the `CMS_API_KEY` environment variable to `.env`:
-
-```bash
-CMS_API_KEY=123
+```ts title="integration-tests/http/test.spec.ts"
+// in your test's file
+jest.setTimeout(60 * 1000)
```
***
-## Next Steps: Sync Brand From Medusa to CMS
+### Run Tests
-You can now use the CMS Module's service to perform actions on the third-party CMS.
+Run the following command to run your tests:
-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.
+```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.
# Environment Variables in Admin Customizations
@@ -7296,6 +7177,125 @@ export default ProductWidget
```
+# 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).
+
+
# Admin Routing Customizations
The Medusa Admin dashboard uses [React Router](https://reactrouter.com) under the hood to manage routing. So, you can have more flexibility in routing-related customizations using some of React Router's utilities, hooks, and components.
@@ -8444,49 +8444,6 @@ The `errorHandler` property's value is a function that accepts four parameters:
This example overrides Medusa's default error handler with a handler that always returns a `400` status code with the same message.
-# HTTP Methods
-
-In this chapter, you'll learn about how to add new API routes for each HTTP method.
-
-## HTTP Method Handler
-
-An API route is created for every HTTP method you export a handler function for in a route file.
-
-Allowed HTTP methods are: `GET`, `POST`, `DELETE`, `PUT`, `PATCH`, `OPTIONS`, and `HEAD`.
-
-For example, create the file `src/api/hello-world/route.ts` with the following content:
-
-```ts title="src/api/hello-world/route.ts"
-import type {
- MedusaRequest,
- MedusaResponse,
-} from "@medusajs/framework/http"
-
-export const GET = async (
- req: MedusaRequest,
- res: MedusaResponse
-) => {
- res.json({
- message: "[GET] Hello world!",
- })
-}
-
-export const POST = async (
- req: MedusaRequest,
- res: MedusaResponse
-) => {
- res.json({
- message: "[POST] Hello world!",
- })
-}
-```
-
-This adds two API Routes:
-
-- A `GET` route at `http://localhost:9000/hello-world`.
-- A `POST` route at `http://localhost:9000/hello-world`.
-
-
# Handling CORS in API Routes
In this chapter, you’ll learn about the CORS middleware and how to configure it for custom API routes.
@@ -8599,6 +8556,220 @@ export default defineMiddlewares({
This retrieves the configurations exported from `medusa-config.ts` and applies the `storeCors` to routes starting with `/custom`.
+# HTTP Methods
+
+In this chapter, you'll learn about how to add new API routes for each HTTP method.
+
+## HTTP Method Handler
+
+An API route is created for every HTTP method you export a handler function for in a route file.
+
+Allowed HTTP methods are: `GET`, `POST`, `DELETE`, `PUT`, `PATCH`, `OPTIONS`, and `HEAD`.
+
+For example, create the file `src/api/hello-world/route.ts` with the following content:
+
+```ts title="src/api/hello-world/route.ts"
+import type {
+ MedusaRequest,
+ MedusaResponse,
+} from "@medusajs/framework/http"
+
+export const GET = async (
+ req: MedusaRequest,
+ res: MedusaResponse
+) => {
+ res.json({
+ message: "[GET] Hello world!",
+ })
+}
+
+export const POST = async (
+ req: MedusaRequest,
+ res: MedusaResponse
+) => {
+ res.json({
+ message: "[POST] Hello world!",
+ })
+}
+```
+
+This adds two API Routes:
+
+- A `GET` route at `http://localhost:9000/hello-world`.
+- A `POST` route at `http://localhost:9000/hello-world`.
+
+
+# Configure Request Body Parser
+
+In this chapter, you'll learn how to configure the request body parser for your API routes.
+
+## Default Body Parser Configuration
+
+The Medusa application configures the body parser by default to parse JSON, URL-encoded, and text request content types. You can parse other data types by adding the relevant [Express middleware](https://expressjs.com/en/guide/using-middleware.html) or preserve the raw body data by configuring the body parser, which is useful for webhook requests.
+
+This chapter shares some examples of configuring the body parser for different data types or use cases.
+
+***
+
+## Preserve Raw Body Data for Webhooks
+
+If your API route receives webhook requests, you might want to preserve the raw body data. To do this, you can configure the body parser to parse the raw body data and store it in the `req.rawBody` property.
+
+To do that, create the file `src/api/middlewares.ts` with the following content:
+
+```ts title="src/api/middlewares.ts" highlights={preserveHighlights}
+import { defineMiddlewares } from "@medusajs/framework/http"
+
+export default defineMiddlewares({
+ routes: [
+ {
+ method: ["POST"],
+ bodyParser: { preserveRawBody: true },
+ matcher: "/custom",
+ },
+ ],
+})
+```
+
+The middleware route object passed to `routes` accepts a `bodyParser` property whose value is an object of configuration for the default body parser. By enabling the `preserveRawBody` property, the raw body data is preserved and stored in the `req.rawBody` property.
+
+Learn more about [middlewares](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md).
+
+You can then access the raw body data in your API route handler:
+
+```ts title="src/api/custom/route.ts"
+import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
+
+export async function POST(
+ req: MedusaRequest,
+ res: MedusaResponse
+) {
+ console.log(req.rawBody)
+
+ // TODO use raw body
+}
+```
+
+***
+
+## Configure Request Body Size Limit
+
+By default, the body parser limits the request body size to `100kb`. If a request body exceeds that size, the Medusa application throws an error.
+
+You can configure the body parser to accept larger request bodies by setting the `sizeLimit` property of the `bodyParser` object in a middleware route object. For example:
+
+```ts title="src/api/middlewares.ts" highlights={sizeLimitHighlights}
+import { defineMiddlewares } from "@medusajs/framework/http"
+
+export default defineMiddlewares({
+ routes: [
+ {
+ method: ["POST"],
+ bodyParser: { sizeLimit: "2mb" },
+ matcher: "/custom",
+ },
+ ],
+})
+```
+
+The `sizeLimit` property accepts one of the following types of values:
+
+- A string representing the size limit in bytes (For example, `100kb`, `2mb`, `5gb`). It is passed to the [bytes](https://www.npmjs.com/package/bytes) library to parse the size.
+- A number representing the size limit in bytes. For example, `1024` for 1kb.
+
+***
+
+## Configure File Uploads
+
+To accept file uploads in your API routes, you can configure the [Express Multer middleware](https://expressjs.com/en/resources/middleware/multer.html) on your route.
+
+The `multer` package is available through the `@medusajs/medusa` package, so you don't need to install it. However, for better typing support, install the `@types/multer` package as a development dependency:
+
+```bash npm2yarn
+npm install --save-dev @types/multer
+```
+
+Then, to configure file upload for your route, create the file `src/api/middlewares.ts` with the following content:
+
+```ts title="src/api/middlewares.ts" highlights={uploadHighlights}
+import { defineMiddlewares } from "@medusajs/framework/http"
+import multer from "multer"
+
+const upload = multer({ storage: multer.memoryStorage() })
+
+export default defineMiddlewares({
+ routes: [
+ {
+ method: ["POST"],
+ matcher: "/custom",
+ middlewares: [
+ // @ts-ignore
+ upload.array("files"),
+ ],
+ },
+ ],
+})
+```
+
+In the example above, you configure the `multer` middleware to store the uploaded files in memory. Then, you apply the `upload.array("files")` middleware to the route to accept file uploads. By using the `array` method, you accept multiple file uploads with the same `files` field name.
+
+You can then access the uploaded files in your API route handler:
+
+```ts title="src/api/custom/route.ts"
+import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
+
+export async function POST(
+ req: MedusaRequest,
+ res: MedusaResponse
+) {
+ const files = req.files as Express.Multer.File[]
+
+ // TODO handle files
+}
+```
+
+The uploaded files are stored in the `req.files` property as an array of Multer file objects that have properties like `filename` and `mimetype`.
+
+### Uploading Files using File Module Provider
+
+The recommended way to upload the files to storage using the configured [File Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/file/index.html.md) is to use the [uploadFilesWorkflow](https://docs.medusajs.com/resources/references/medusa-workflows/uploadFilesWorkflow/index.html.md):
+
+```ts title="src/api/custom/route.ts"
+import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
+import { MedusaError } from "@medusajs/framework/utils"
+import { uploadFilesWorkflow } from "@medusajs/medusa/core-flows"
+
+export async function POST(
+ req: MedusaRequest,
+ res: MedusaResponse
+) {
+ const files = req.files as Express.Multer.File[]
+
+ if (!files?.length) {
+ throw new MedusaError(
+ MedusaError.Types.INVALID_DATA,
+ "No files were uploaded"
+ )
+ }
+
+ const { result } = await uploadFilesWorkflow(req.scope).run({
+ input: {
+ files: files?.map((f) => ({
+ filename: f.originalname,
+ mimeType: f.mimetype,
+ content: f.buffer.toString("binary"),
+ access: "public",
+ })),
+ },
+ })
+
+ res.status(200).json({ files: result })
+}
+```
+
+Check out the [uploadFilesWorkflow reference](https://docs.medusajs.com/resources/references/medusa-workflows/uploadFilesWorkflow/index.html.md) for details on the expected input and output of the workflow.
+
+
# API Route Parameters
In this chapter, you’ll learn about path, query, and request body parameters.
@@ -9175,177 +9346,6 @@ If you need to change the middlewares applied to a route, you can create a custo
Learn more in the [Override API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/override/index.html.md) chapter.
-# Configure Request Body Parser
-
-In this chapter, you'll learn how to configure the request body parser for your API routes.
-
-## Default Body Parser Configuration
-
-The Medusa application configures the body parser by default to parse JSON, URL-encoded, and text request content types. You can parse other data types by adding the relevant [Express middleware](https://expressjs.com/en/guide/using-middleware.html) or preserve the raw body data by configuring the body parser, which is useful for webhook requests.
-
-This chapter shares some examples of configuring the body parser for different data types or use cases.
-
-***
-
-## Preserve Raw Body Data for Webhooks
-
-If your API route receives webhook requests, you might want to preserve the raw body data. To do this, you can configure the body parser to parse the raw body data and store it in the `req.rawBody` property.
-
-To do that, create the file `src/api/middlewares.ts` with the following content:
-
-```ts title="src/api/middlewares.ts" highlights={preserveHighlights}
-import { defineMiddlewares } from "@medusajs/framework/http"
-
-export default defineMiddlewares({
- routes: [
- {
- method: ["POST"],
- bodyParser: { preserveRawBody: true },
- matcher: "/custom",
- },
- ],
-})
-```
-
-The middleware route object passed to `routes` accepts a `bodyParser` property whose value is an object of configuration for the default body parser. By enabling the `preserveRawBody` property, the raw body data is preserved and stored in the `req.rawBody` property.
-
-Learn more about [middlewares](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md).
-
-You can then access the raw body data in your API route handler:
-
-```ts title="src/api/custom/route.ts"
-import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
-
-export async function POST(
- req: MedusaRequest,
- res: MedusaResponse
-) {
- console.log(req.rawBody)
-
- // TODO use raw body
-}
-```
-
-***
-
-## Configure Request Body Size Limit
-
-By default, the body parser limits the request body size to `100kb`. If a request body exceeds that size, the Medusa application throws an error.
-
-You can configure the body parser to accept larger request bodies by setting the `sizeLimit` property of the `bodyParser` object in a middleware route object. For example:
-
-```ts title="src/api/middlewares.ts" highlights={sizeLimitHighlights}
-import { defineMiddlewares } from "@medusajs/framework/http"
-
-export default defineMiddlewares({
- routes: [
- {
- method: ["POST"],
- bodyParser: { sizeLimit: "2mb" },
- matcher: "/custom",
- },
- ],
-})
-```
-
-The `sizeLimit` property accepts one of the following types of values:
-
-- A string representing the size limit in bytes (For example, `100kb`, `2mb`, `5gb`). It is passed to the [bytes](https://www.npmjs.com/package/bytes) library to parse the size.
-- A number representing the size limit in bytes. For example, `1024` for 1kb.
-
-***
-
-## Configure File Uploads
-
-To accept file uploads in your API routes, you can configure the [Express Multer middleware](https://expressjs.com/en/resources/middleware/multer.html) on your route.
-
-The `multer` package is available through the `@medusajs/medusa` package, so you don't need to install it. However, for better typing support, install the `@types/multer` package as a development dependency:
-
-```bash npm2yarn
-npm install --save-dev @types/multer
-```
-
-Then, to configure file upload for your route, create the file `src/api/middlewares.ts` with the following content:
-
-```ts title="src/api/middlewares.ts" highlights={uploadHighlights}
-import { defineMiddlewares } from "@medusajs/framework/http"
-import multer from "multer"
-
-const upload = multer({ storage: multer.memoryStorage() })
-
-export default defineMiddlewares({
- routes: [
- {
- method: ["POST"],
- matcher: "/custom",
- middlewares: [
- // @ts-ignore
- upload.array("files"),
- ],
- },
- ],
-})
-```
-
-In the example above, you configure the `multer` middleware to store the uploaded files in memory. Then, you apply the `upload.array("files")` middleware to the route to accept file uploads. By using the `array` method, you accept multiple file uploads with the same `files` field name.
-
-You can then access the uploaded files in your API route handler:
-
-```ts title="src/api/custom/route.ts"
-import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
-
-export async function POST(
- req: MedusaRequest,
- res: MedusaResponse
-) {
- const files = req.files as Express.Multer.File[]
-
- // TODO handle files
-}
-```
-
-The uploaded files are stored in the `req.files` property as an array of Multer file objects that have properties like `filename` and `mimetype`.
-
-### Uploading Files using File Module Provider
-
-The recommended way to upload the files to storage using the configured [File Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/file/index.html.md) is to use the [uploadFilesWorkflow](https://docs.medusajs.com/resources/references/medusa-workflows/uploadFilesWorkflow/index.html.md):
-
-```ts title="src/api/custom/route.ts"
-import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
-import { MedusaError } from "@medusajs/framework/utils"
-import { uploadFilesWorkflow } from "@medusajs/medusa/core-flows"
-
-export async function POST(
- req: MedusaRequest,
- res: MedusaResponse
-) {
- const files = req.files as Express.Multer.File[]
-
- if (!files?.length) {
- throw new MedusaError(
- MedusaError.Types.INVALID_DATA,
- "No files were uploaded"
- )
- }
-
- const { result } = await uploadFilesWorkflow(req.scope).run({
- input: {
- files: files?.map((f) => ({
- filename: f.originalname,
- mimeType: f.mimetype,
- content: f.buffer.toString("binary"),
- access: "public",
- })),
- },
- })
-
- res.status(200).json({ files: result })
-}
-```
-
-Check out the [uploadFilesWorkflow reference](https://docs.medusajs.com/resources/references/medusa-workflows/uploadFilesWorkflow/index.html.md) for details on the expected input and output of the workflow.
-
-
# Retrieve Custom Links from Medusa's API Route
In this chapter, you'll learn how to retrieve custom data models linked to existing Medusa data models from Medusa's API routes.
@@ -10044,51 +10044,6 @@ 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).
-# Event Data Payload
-
-In this chapter, you'll learn how subscribers receive an event's data payload.
-
-## Access Event's Data Payload
-
-When events are emitted, they’re emitted with a data payload.
-
-The object that the subscriber function receives as a parameter has an `event` property, which is an object holding the event payload in a `data` property with additional context.
-
-For example:
-
-```ts title="src/subscribers/product-created.ts" highlights={highlights} collapsibleLines="1-5" expandButtonLabel="Show Imports"
-import type {
- SubscriberArgs,
- SubscriberConfig,
-} from "@medusajs/framework"
-
-export default async function productCreateHandler({
- event,
-}: SubscriberArgs<{ id: string }>) {
- const productId = event.data.id
- console.log(`The product ${productId} was created`)
-}
-
-export const config: SubscriberConfig = {
- event: "product.created",
-}
-```
-
-The `event` object has the following properties:
-
-- data: (\`object\`) The data payload of the event. Its properties are different for each event.
-- name: (string) The name of the triggered event.
-- metadata: (\`object\`) Additional data and context of the emitted event.
-
-This logs the product ID received in the `product.created` event’s data payload to the console.
-
-{/* ---
-
-## List of Events with Data Payload
-
-Refer to [this reference](!resources!/references/events) for a full list of events emitted by Medusa and their data payloads. */}
-
-
# Emit Workflow and Service Events
In this chapter, you'll learn about event types and how to emit an event in a service or workflow.
@@ -10230,6 +10185,8 @@ The method accepts an object having the following properties:
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:
+### Module Registration
+
```ts title="medusa-config.ts" highlights={depsHighlight}
import { Modules } from "@medusajs/framework/utils"
@@ -10246,9 +10203,38 @@ module.exports = defineConfig({
})
```
+### Module Provider Registration
+
+```ts title="medusa-config.ts" highlights={depsHighlight}
+import { Modules } from "@medusajs/framework/utils"
+
+module.exports = defineConfig({
+ // ...
+ modules: [
+ {
+ resolve: "@medusajs/medusa/payment",
+ dependencies: [
+ Modules.EVENT_BUS,
+ ],
+ options: {
+ providers: [
+ {
+ resolve: "./src/modules/my-provider",
+ id: "my-provider",
+ options: {
+ // ...
+ },
+ },
+ ],
+ },
+ },
+ ],
+})
+```
+
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.
+If a module has providers, the dependencies are also injected into the providers' containers.
### Test it Out
@@ -10257,6 +10243,485 @@ If you execute the `performAction` method of your service, the event is emitted
Any subscribers listening to the event are also executed.
+# Event Data Payload
+
+In this chapter, you'll learn how subscribers receive an event's data payload.
+
+## Access Event's Data Payload
+
+When events are emitted, they’re emitted with a data payload.
+
+The object that the subscriber function receives as a parameter has an `event` property, which is an object holding the event payload in a `data` property with additional context.
+
+For example:
+
+```ts title="src/subscribers/product-created.ts" highlights={highlights} collapsibleLines="1-5" expandButtonLabel="Show Imports"
+import type {
+ SubscriberArgs,
+ SubscriberConfig,
+} from "@medusajs/framework"
+
+export default async function productCreateHandler({
+ event,
+}: SubscriberArgs<{ id: string }>) {
+ const productId = event.data.id
+ console.log(`The product ${productId} was created`)
+}
+
+export const config: SubscriberConfig = {
+ event: "product.created",
+}
+```
+
+The `event` object has the following properties:
+
+- data: (\`object\`) The data payload of the event. Its properties are different for each event.
+- name: (string) The name of the triggered event.
+- metadata: (\`object\`) Additional data and context of the emitted event.
+
+This logs the product ID received in the `product.created` event’s data payload to the console.
+
+{/* ---
+
+## List of Events with Data Payload
+
+Refer to [this reference](!resources!/references/events) for a full list of events emitted by Medusa and their data payloads. */}
+
+
+# Create a Plugin
+
+In this chapter, you'll learn how to create a Medusa plugin and publish it.
+
+A [plugin](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md) is a package of reusable Medusa customizations that you can install in any Medusa application. By creating and publishing a plugin, you can reuse your Medusa customizations across multiple projects or share them with the community.
+
+Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0).
+
+## 1. Create a Plugin Project
+
+Plugins are created in a separate Medusa project. This makes the development and publishing of the plugin easier. Later, you'll install that plugin in your Medusa application to test it out and use it.
+
+Medusa's `create-medusa-app` CLI tool provides the option to create a plugin project. Run the following command to create a new plugin project:
+
+```bash
+npx create-medusa-app my-plugin --plugin
+```
+
+This will create a new Medusa plugin project in the `my-plugin` directory.
+
+### Plugin Directory Structure
+
+After the installation is done, the plugin structure will look like this:
+
+
+
+- `src/`: Contains the Medusa customizations.
+- `src/admin`: Contains [admin extensions](https://docs.medusajs.com/learn/fundamentals/admin/index.html.md).
+- `src/api`: Contains [API routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) and [middlewares](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md). You can add store, admin, or any custom API routes.
+- `src/jobs`: Contains [scheduled jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md).
+- `src/links`: Contains [module links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md).
+- `src/modules`: Contains [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md).
+- `src/provider`: Contains [module providers](#create-module-providers).
+- `src/subscribers`: Contains [subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md).
+- `src/workflows`: Contains [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). You can also add [hooks](https://docs.medusajs.com/learn/fundamentals/workflows/add-workflow-hook/index.html.md) under `src/workflows/hooks`.
+- `package.json`: Contains the plugin's package information, including general information and dependencies.
+- `tsconfig.json`: Contains the TypeScript configuration for the plugin.
+
+***
+
+## 2. Prepare Plugin
+
+### Package Name
+
+Before developing, testing, and publishing your plugin, make sure its name in `package.json` is correct. This is the name you'll use to install the plugin in your Medusa application.
+
+For example:
+
+```json title="package.json"
+{
+ "name": "@myorg/plugin-name",
+ // ...
+}
+```
+
+### Package Keywords
+
+Medusa scrapes NPM for a list of plugins that integrate third-party services, to later showcase them on the Medusa website. If you want your plugin to appear in that listing, make sure to add the `medusa-v2` and `medusa-plugin-integration` keywords to the `keywords` field in `package.json`.
+
+Only plugins that integrate third-party services are listed in the Medusa integrations page. If your plugin doesn't integrate a third-party service, you can skip this step.
+
+```json title="package.json"
+{
+ "keywords": [
+ "medusa-plugin-integration",
+ "medusa-v2"
+ ],
+ // ...
+}
+```
+
+In addition, make sure to use one of the following keywords based on your integration type:
+
+|Keyword|Description|Example|
+|---|---|---|
+|\`medusa-plugin-analytics\`|Analytics service integration|Google Analytics|
+|\`medusa-plugin-auth\`|Authentication service integration|Auth0|
+|\`medusa-plugin-cms\`|CMS service integration|Contentful|
+|\`medusa-plugin-notification\`|Notification service integration|Twilio SMS|
+|\`medusa-plugin-payment\`|Payment service integration|PayPal|
+|\`medusa-plugin-search\`|Search service integration|MeiliSearch|
+|\`medusa-plugin-shipping\`|Shipping service integration|DHL|
+|\`medusa-plugin-other\`|Other third-party integrations|Sentry|
+
+### Package Dependencies
+
+Your plugin project will already have the dependencies mentioned in this section. Unless you made changes to the dependencies, you can skip this section.
+
+In the `package.json` file you must have the Medusa dependencies as `devDependencies` and `peerDependencies`. In addition, you must have `@swc/core` as a `devDependency`, as it's used by the plugin CLI tools.
+
+For example, assuming `2.5.0` is the latest Medusa version:
+
+```json title="package.json"
+{
+ "devDependencies": {
+ "@medusajs/admin-sdk": "2.5.0",
+ "@medusajs/cli": "2.5.0",
+ "@medusajs/framework": "2.5.0",
+ "@medusajs/medusa": "2.5.0",
+ "@medusajs/test-utils": "2.5.0",
+ "@medusajs/ui": "4.0.4",
+ "@medusajs/icons": "2.5.0",
+ "@swc/core": "1.5.7",
+ },
+ "peerDependencies": {
+ "@medusajs/admin-sdk": "2.5.0",
+ "@medusajs/cli": "2.5.0",
+ "@medusajs/framework": "2.5.0",
+ "@medusajs/test-utils": "2.5.0",
+ "@medusajs/medusa": "2.5.0",
+ "@medusajs/ui": "4.0.3",
+ "@medusajs/icons": "2.5.0",
+ }
+}
+```
+
+### Package Exports
+
+Your plugin project will already have the exports mentioned in this section. Unless you made changes to the exports or you created your plugin before [Medusa v2.7.0](https://github.com/medusajs/medusa/releases/tag/v2.7.0), you can skip this section.
+
+In the `package.json` file, make sure your plugin has the following exports:
+
+```json title="package.json"
+{
+ "exports": {
+ "./package.json": "./package.json",
+ "./workflows": "./.medusa/server/src/workflows/index.js",
+ "./.medusa/server/src/modules/*": "./.medusa/server/src/modules/*/index.js",
+ "./providers/*": "./.medusa/server/src/providers/*/index.js",
+ "./admin": {
+ "import": "./.medusa/server/src/admin/index.mjs",
+ "require": "./.medusa/server/src/admin/index.js",
+ "default": "./.medusa/server/src/admin/index.js"
+ },
+ "./*": "./.medusa/server/src/*.js"
+ }
+}
+```
+
+Aside from the `./package.json`, `./providers`, and `./admin`, these exports are only a recommendation. You can cherry-pick the files and directories you want to export.
+
+The plugin exports the following files and directories:
+
+- `./package.json`: The `package.json` file. Medusa needs to access the `package.json` when registering the plugin.
+- `./workflows`: The workflows exported in `./src/workflows/index.ts`.
+- `./.medusa/server/src/modules/*`: The definition file of modules. This is useful if you create links to the plugin's modules in the Medusa application.
+- `./providers/*`: The definition file of module providers. This is useful if your plugin includes a module provider, allowing you to register the plugin's providers in Medusa applications. Learn more in the [Create Module Providers](#create-module-providers) section.
+- `./admin`: The admin extensions exported in `./src/admin/index.ts`.
+- `./*`: Any other files in the plugin's `src` directory.
+
+***
+
+## 3. Publish Plugin Locally for Development and Testing
+
+Medusa's CLI tool provides commands to simplify developing and testing your plugin in a local Medusa application. You start by publishing your plugin in the local package registry, then install it in your Medusa application. You can then watch for changes in the plugin as you develop it.
+
+### Publish and Install Local Package
+
+### Prerequisites
+
+- [Medusa application installed.](https://docs.medusajs.com/learn/installation/index.html.md)
+
+The first time you create your plugin, you need to publish the package into a local package registry, then install it in your Medusa application. This is a one-time only process.
+
+To publish the plugin to the local registry, run the following command in your plugin project:
+
+```bash title="Plugin project"
+npx medusa plugin:publish
+```
+
+This command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. The plugin is published locally under the name you specified in `package.json`.
+
+Next, navigate to your Medusa application:
+
+```bash title="Medusa application"
+cd ~/path/to/medusa-app
+```
+
+Make sure to replace `~/path/to/medusa-app` with the path to your Medusa application.
+
+Then, if your project was created before v2.3.1 of Medusa, make sure to install `yalc` as a development dependency:
+
+```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green"
+npm install --save-dev yalc
+```
+
+After that, run the following Medusa CLI command to install the plugin:
+
+```bash title="Medusa application"
+npx medusa plugin:add @myorg/plugin-name
+```
+
+Make sure to replace `@myorg/plugin-name` with the name of your plugin as specified in `package.json`. Your plugin will be installed from the local package registry into your Medusa application.
+
+### Register Plugin in Medusa Application
+
+After installing the plugin, you need to register it in your Medusa application in the configurations defined in `medusa-config.ts`.
+
+Add the plugin to the `plugins` array in the `medusa-config.ts` file:
+
+```ts title="medusa-config.ts" highlights={pluginHighlights}
+module.exports = defineConfig({
+ // ...
+ plugins: [
+ {
+ resolve: "@myorg/plugin-name",
+ options: {},
+ },
+ ],
+})
+```
+
+The `plugins` configuration is an array of objects where each object has a `resolve` key whose value is the name of the plugin package.
+
+#### Pass Module Options through Plugin
+
+Each plugin configuration also accepts an `options` property, whose value is an object of options to pass to the plugin's modules.
+
+For example:
+
+```ts title="medusa-config.ts" highlights={pluginOptionsHighlight}
+module.exports = defineConfig({
+ // ...
+ plugins: [
+ {
+ resolve: "@myorg/plugin-name",
+ options: {
+ apiKey: true,
+ },
+ },
+ ],
+})
+```
+
+The `options` property in the plugin configuration is passed to all modules in the plugin. Learn more about module options in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/options/index.html.md).
+
+### Watch Plugin Changes During Development
+
+While developing your plugin, you can watch for changes in the plugin and automatically update the plugin in the Medusa application using it. This is the only command you'll continuously need during your plugin development.
+
+To do that, run the following command in your plugin project:
+
+```bash title="Plugin project"
+npx medusa plugin:develop
+```
+
+This command will:
+
+- Watch for changes in the plugin. Whenever a file is changed, the plugin is automatically built.
+- Publish the plugin changes to the local package registry. This will automatically update the plugin in the Medusa application using it. You can also benefit from real-time HMR updates of admin extensions.
+
+### Start Medusa Application
+
+You can start your Medusa application's development server to test out your plugin:
+
+```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green"
+npm run dev
+```
+
+While your Medusa application is running and the plugin is being watched, you can test your plugin while developing it in the Medusa application.
+
+***
+
+## 4. Create Customizations in the Plugin
+
+You can now build your plugin's customizations. The following guide explains how to build different customizations in your plugin.
+
+- [Create a module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md)
+- [Create a module link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md)
+- [Create a workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md)
+- [Add a workflow hook](https://docs.medusajs.com/learn/fundamentals/workflows/add-workflow-hook/index.html.md)
+- [Create an API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md)
+- [Add a subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md)
+- [Add a scheduled job](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md)
+- [Add an admin widget](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md)
+- [Add an admin UI route](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md)
+
+While building those customizations, you can test them in your Medusa application by [watching the plugin changes](#watch-plugin-changes-during-development) and [starting the Medusa application](#start-medusa-application).
+
+### Generating Migrations for Modules
+
+During your development, you may need to generate migrations for modules in your plugin. To do that, first, add the following environment variables in your plugin project:
+
+```plain title="Plugin project"
+DB_USERNAME=postgres
+DB_PASSWORD=123...
+DB_HOST=localhost
+DB_PORT=5432
+DB_NAME=db_name
+```
+
+You can add these environment variables in a `.env` file in your plugin project. The variables are:
+
+- `DB_USERNAME`: The username of the PostgreSQL user to connect to the database.
+- `DB_PASSWORD`: The password of the PostgreSQL user to connect to the database.
+- `DB_HOST`: The host of the PostgreSQL database. Typically, it's `localhost` if you're running the database locally.
+- `DB_PORT`: The port of the PostgreSQL database. Typically, it's `5432` if you're running the database locally.
+- `DB_NAME`: The name of the PostgreSQL database to connect to.
+
+Then, run the following command in your plugin project to generate migrations for the modules in your plugin:
+
+```bash title="Plugin project"
+npx medusa plugin:db:generate
+```
+
+This command generates migrations for all modules in the plugin.
+
+Finally, run these migrations on the Medusa application that the plugin is installed in using the `db:migrate` command:
+
+```bash title="Medusa application"
+npx medusa db:migrate
+```
+
+The migrations in your application, including your plugin, will run and update the database.
+
+### Importing Module Resources
+
+In the [Prepare Plugin](#2-prepare-plugin) section, you learned about [exported resources](#package-exports) in your plugin.
+
+These exports allow you to import your plugin resources in your Medusa application, including workflows, links and modules.
+
+For example, to import the plugin's workflow in your Medusa application:
+
+`@myorg/plugin-name` is the plugin package's name.
+
+```ts
+import { Workflow1, Workflow2 } from "@myorg/plugin-name/workflows"
+import BlogModule from "@myorg/plugin-name/modules/blog"
+// import other files created in plugin like ./src/types/blog.ts
+import BlogType from "@myorg/plugin-name/types/blog"
+```
+
+### Create Module Providers
+
+The [exported resources](#package-exports) also allow you to import module providers in your plugin and register them in the Medusa application's configuration. A module provider is a module that provides the underlying logic or integration related to a commerce or Infrastructure Module.
+
+For example, assuming your plugin has a [Notification Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/notification/index.html.md) called `my-notification`, you can register it in your Medusa application's configuration like this:
+
+`@myorg/plugin-name` is the plugin package's name.
+
+```ts highlights={[["9"]]} title="medusa-config.ts"
+module.exports = defineConfig({
+ // ...
+ modules: [
+ {
+ resolve: "@medusajs/medusa/notification",
+ options: {
+ providers: [
+ {
+ resolve: "@myorg/plugin-name/providers/my-notification",
+ id: "my-notification",
+ options: {
+ channels: ["email"],
+ // provider options...
+ },
+ },
+ ],
+ },
+ },
+ ],
+})
+```
+
+You pass to `resolve` the path to the provider relative to the plugin package. So, in this example, the `my-notification` provider is located in `./src/providers/my-notification/index.ts` of the plugin.
+
+To learn how to create module providers, refer to the following guides:
+
+- [File Module Provider](https://docs.medusajs.com/resources/references/file-provider-module/index.html.md)
+- [Notification Module Provider](https://docs.medusajs.com/resources/references/notification-provider-module/index.html.md)
+- [Auth Module Provider](https://docs.medusajs.com/resources/references/auth/provider/index.html.md)
+- [Payment Module Provider](https://docs.medusajs.com/resources/references/payment/provider/index.html.md)
+- [Fulfillment Module Provider](https://docs.medusajs.com/resources/references/fulfillment/provider/index.html.md)
+- [Tax Module Provider](https://docs.medusajs.com/resources/references/tax/provider/index.html.md)
+
+***
+
+## 5. Publish Plugin to NPM
+
+Make sure to add the keywords mentioned in the [Package Keywords](#package-keywords) section in your plugin's `package.json` file.
+
+Medusa's CLI tool provides a command that bundles your plugin to be published to npm. Once you're ready to publish your plugin publicly, run the following command in your plugin project:
+
+```bash
+npx medusa plugin:build
+```
+
+The command will compile an output in the `.medusa/server` directory.
+
+You can now publish the plugin to npm using the [NPM CLI tool](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). Run the following command to publish the plugin to npm:
+
+```bash
+npm publish
+```
+
+If you haven't logged in before with your NPM account, you'll be asked to log in first. Then, your package is published publicly to be used in any Medusa application.
+
+### Install Public Plugin in Medusa Application
+
+You install a plugin that's published publicly using your package manager. For example:
+
+```bash npm2yarn
+npm install @myorg/plugin-name
+```
+
+Where `@myorg/plugin-name` is the name of your plugin as published on NPM.
+
+Then, register the plugin in your Medusa application's configurations as explained in [this section](#register-plugin-in-medusa-application).
+
+***
+
+## Update a Published Plugin
+
+To update the Medusa dependencies in a plugin, refer to [this documentation](https://docs.medusajs.com/learn/update#update-plugin-project/index.html.md).
+
+If you've published a plugin and you've made changes to it, you'll have to publish the update to NPM again.
+
+First, run the following command to change the version of the plugin:
+
+```bash
+npm version
+```
+
+Where `` indicates the type of version update you’re publishing. For example, it can be `major` or `minor`. Refer to the [npm version documentation](https://docs.npmjs.com/cli/v10/commands/npm-version) for more information.
+
+Then, re-run the same commands for publishing a plugin:
+
+```bash
+npx medusa plugin:build
+npm publish
+```
+
+This will publish an updated version of your plugin under a new version.
+
+
# Add Data Model Check Constraints
In this chapter, you'll learn how to add check constraints to your data model.
@@ -10329,6 +10794,46 @@ npx medusa db:migrate
The first command generates the migration under the `migrations` directory of your module's directory, and the second reflects it on the database.
+# Infer Type of Data Model
+
+In this chapter, you'll learn how to infer the type of a data model.
+
+## How to Infer Type of Data Model?
+
+Consider you have a `Post` data model. You can't reference this data model in a type, such as a workflow input or service method output types, since it's a variable.
+
+Instead, Medusa provides `InferTypeOf` that transforms your data model to a type.
+
+For example:
+
+```ts
+import { InferTypeOf } from "@medusajs/framework/types"
+import { Post } from "../modules/blog/models/post" // relative path to the model
+
+export type Post = InferTypeOf
+```
+
+`InferTypeOf` accepts as a type argument the type of the data model.
+
+Since the `Post` data model is a variable, use the `typeof` operator to pass the data model as a type argument to `InferTypeOf`.
+
+You can now use the `Post` type to reference a data model in other types, such as in workflow inputs or service method outputs:
+
+```ts title="Example Service"
+// other imports...
+import { InferTypeOf } from "@medusajs/framework/types"
+import { Post } from "../models/post"
+
+type Post = InferTypeOf
+
+class BlogModuleService extends MedusaService({ Post }) {
+ async doSomething(): Promise {
+ // ...
+ }
+}
+```
+
+
# Data Model Database Index
In this chapter, you’ll learn how to define a database index on a data model.
@@ -10441,46 +10946,6 @@ 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.
@@ -11053,6 +11518,323 @@ const posts = await blogModuleService.listPosts({
This retrieves records that include `New Products` in their `title` property.
+# Migrations
+
+In this chapter, you'll learn what a migration is and how to generate a migration or write it manually.
+
+## What is a Migration?
+
+A migration is a TypeScript or JavaScript file that defines database changes made by a module. Migrations are useful when you re-use a module or you're working in a team, so that when one member of a team makes a database change, everyone else can reflect it on their side by running the migrations.
+
+The migration's file has a class with two methods:
+
+- The `up` method reflects changes on the database.
+- The `down` method reverts the changes made in the `up` method.
+
+***
+
+## Generate Migration
+
+Instead of you writing the migration manually, the Medusa CLI tool provides a [db:generate](https://docs.medusajs.com/resources/medusa-cli/commands/db#dbgenerate/index.html.md) command to generate a migration for a modules' data models.
+
+For example, assuming you have a `blog` Module, you can generate a migration for it by running the following command:
+
+```bash
+npx medusa db:generate blog
+```
+
+This generates a migration file under the `migrations` directory of the Blog Module. You can then run it to reflect the changes in the database as mentioned in [this section](#run-the-migration).
+
+***
+
+## Write a Migration Manually
+
+You can also write migrations manually. To do that, create a file in the `migrations` directory of the module and in it, a class that has an `up` and `down` method. The class's name should be of the format `Migration{YEAR}{MONTH}{DAY}{HOUR}{MINUTE}.ts` to ensure migrations are ran in the correct order.
+
+For example:
+
+```ts title="src/modules/blog/migrations/Migration202507021059.ts"
+import { Migration } from "@mikro-orm/migrations"
+
+export class Migration202507021059 extends Migration {
+
+ async up(): Promise {
+ this.addSql("create table if not exists \"author\" (\"id\" text not null, \"name\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"author_pkey\" primary key (\"id\"));")
+ }
+
+ async down(): Promise {
+ this.addSql("drop table if exists \"author\" cascade;")
+ }
+
+}
+```
+
+The migration class in the file extends the `Migration` class imported from `@mikro-orm/migrations`. In the `up` and `down` method of the migration class, you use the `addSql` method provided by MikroORM's `Migration` class to run PostgreSQL syntax.
+
+In the example above, the `up` method creates the table `author`, and the `down` method drops the table if the migration is reverted.
+
+Refer to [MikroORM's documentation](https://mikro-orm.io/docs/migrations#migration-class) for more details on writing migrations.
+
+***
+
+## Run the Migration
+
+To run your migration, run the following command:
+
+This command also syncs module links. If you don't want that, use the `--skip-links` option.
+
+```bash
+npx medusa db:migrate
+```
+
+This reflects the changes in the database as implemented in the migration's `up` method.
+
+***
+
+## Rollback the Migration
+
+To rollback or revert the last migration you ran for a module, run the following command:
+
+```bash
+npx medusa db:rollback blog
+```
+
+This rolls back the last ran migration on the Blog Module.
+
+### Caution: Rollback Migration before Deleting
+
+If you need to delete a migration file, make sure to rollback the migration first. Otherwise, you might encounter issues when generating and running new migrations.
+
+For example, if you delete the migration of the Blog Module, then try to create a new one, Medusa will create a brand new migration that re-creates the tables or indices. If those are still in the database, you might encounter errors.
+
+So, always rollback the migration before deleting it.
+
+***
+
+## More Database Commands
+
+To learn more about the Medusa CLI's database commands, refer to [this CLI reference](https://docs.medusajs.com/resources/medusa-cli/commands/db/index.html.md).
+
+
+# Module Link Direction
+
+In this chapter, you'll learn about the difference in module link directions, and which to use based on your use case.
+
+The details in this chapter don't apply to [Read-Only Module Links](https://docs.medusajs.com/learn/fundamentals/module-links/read-only/index.html.md). Refer to the [Read-Only Module Links chapter](https://docs.medusajs.com/learn/fundamentals/module-links/read-only/index.html.md) for more information on read-only links and their direction.
+
+## Link Direction
+
+The module link's direction depends on the order you pass the data model configuration parameters to `defineLink`.
+
+For example, the following defines a link from the Blog Module's `post` data model to the Product Module's `product` data model:
+
+```ts
+export default defineLink(
+ BlogModule.linkable.post,
+ ProductModule.linkable.product
+)
+```
+
+Whereas the following defines a link from the Product Module's `product` data model to the Blog Module's `post` data model:
+
+```ts
+export default defineLink(
+ ProductModule.linkable.product,
+ BlogModule.linkable.post
+)
+```
+
+The above links are two different links that serve different purposes.
+
+***
+
+## Which Link Direction to Use?
+
+### Extend Data Models
+
+If you're adding a link to a data model to extend it and add new fields, define the link from the main data model to the custom data model.
+
+For example, consider you want to add a `subtitle` custom field to the `product` data model. To do that, you define a `Subtitle` data model in your module, then define a link from the `Product` data model to it:
+
+```ts
+export default defineLink(
+ ProductModule.linkable.product,
+ BlogModule.linkable.subtitle
+)
+```
+
+### Associate Data Models
+
+If you're linking data models to indicate an association between them, define the link from the custom data model to the main data model.
+
+For example, consider you have `Post` data model representing a blog post, and you want to associate a blog post with a product. To do that, define a link from the `Post` data model to `Product`:
+
+```ts
+export default defineLink(
+ BlogModule.linkable.post,
+ ProductModule.linkable.product
+)
+```
+
+
+# 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.
+
+## Link Table's Default Columns
+
+When you define a link between two data models, Medusa creates a link table in the database to store the IDs of the linked records. You can learn more about the created table in the [Module Links chapter](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md).
+
+In various cases, you might need to store additional data in the link table. For example, if you define a link between a `product` and a `post`, you might want to store the publish date of the product's post in the link table.
+
+In those cases, you can add a custom column to a link's table in the link definition. You can later set that column whenever you create or update a link between the linked records.
+
+***
+
+## How to Add Custom Columns to a Link's Table?
+
+The `defineLink` function used to define a link accepts a third parameter, which is an object of options.
+
+To add custom columns to a link's table, pass in the third parameter of `defineLink` a `database` property:
+
+```ts highlights={linkHighlights}
+import BlogModule from "../modules/blog"
+import ProductModule from "@medusajs/medusa/product"
+import { defineLink } from "@medusajs/framework/utils"
+
+export default defineLink(
+ ProductModule.linkable.product,
+ BlogModule.linkable.blog,
+ {
+ database: {
+ extraColumns: {
+ metadata: {
+ type: "json",
+ },
+ },
+ },
+ }
+)
+```
+
+This adds to the table created for the link between `product` and `blog` a `metadata` column of type `json`.
+
+### Database Options
+
+The `database` property defines configuration for the table created in the database.
+
+Its `extraColumns` property defines custom columns to create in the link's table.
+
+`extraColumns`'s value is an object whose keys are the names of the columns, and values are the column's configurations as an object.
+
+### Column Configurations
+
+The column's configurations object accepts the following properties:
+
+- `type`: The column's type. Possible values are:
+ - `string`
+ - `text`
+ - `integer`
+ - `boolean`
+ - `date`
+ - `time`
+ - `datetime`
+ - `enum`
+ - `json`
+ - `array`
+ - `enumArray`
+ - `float`
+ - `double`
+ - `decimal`
+ - `bigint`
+ - `mediumint`
+ - `smallint`
+ - `tinyint`
+ - `blob`
+ - `uuid`
+ - `uint8array`
+- `defaultValue`: The column's default value.
+- `nullable`: Whether the column can have `null` values.
+
+***
+
+## Set Custom Column when Creating Link
+
+The object you pass to Link's `create` method accepts a `data` property. Its value is an object whose keys are custom column names, and values are the value of the custom column for this link.
+
+For example:
+
+Learn more about Link, how to resolve it, and its methods in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/link/index.html.md).
+
+```ts
+await link.create({
+ [Modules.PRODUCT]: {
+ product_id: "123",
+ },
+ [BLOG_MODULE]: {
+ post_id: "321",
+ },
+ data: {
+ metadata: {
+ test: true,
+ },
+ },
+})
+```
+
+***
+
+## Retrieve Custom Column with Link
+
+To retrieve linked records with their custom columns, use [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). A module link's definition, exported by a file under `src/links`, has a special `entryPoint` property. Use this property when specifying the `entity` property in Query's `graph` method.
+
+For example:
+
+```ts highlights={retrieveHighlights}
+import productPostLink from "../links/product-post"
+
+// ...
+
+const { data } = await query.graph({
+ entity: productPostLink.entryPoint,
+ fields: ["metadata", "product.*", "post.*"],
+ filters: {
+ product_id: "prod_123",
+ },
+})
+```
+
+This retrieves the product of id `prod_123` and its linked `post` records.
+
+In the `fields` array you pass `metadata`, which is the custom column to retrieve of the link.
+
+***
+
+## Update Custom Column's Value
+
+Link's `create` method updates a link's data if the link between the specified records already exists.
+
+So, to update the value of a custom column in a created link, use the `create` method again passing it a new value for the custom column.
+
+For example:
+
+```ts
+await link.create({
+ [Modules.PRODUCT]: {
+ product_id: "123",
+ },
+ [BLOG_MODULE]: {
+ post_id: "321",
+ },
+ data: {
+ metadata: {
+ test: false,
+ },
+ },
+})
+```
+
+
# Data Model Relationships
In this chapter, you’ll learn how to define relationships between data models in your module.
@@ -11348,323 +12130,6 @@ The `cascades` method accepts an object. Its key is the operation’s name, such
In the example above, when a store is deleted, its associated products are also deleted.
-# Migrations
-
-In this chapter, you'll learn what a migration is and how to generate a migration or write it manually.
-
-## What is a Migration?
-
-A migration is a TypeScript or JavaScript file that defines database changes made by a module. Migrations are useful when you re-use a module or you're working in a team, so that when one member of a team makes a database change, everyone else can reflect it on their side by running the migrations.
-
-The migration's file has a class with two methods:
-
-- The `up` method reflects changes on the database.
-- The `down` method reverts the changes made in the `up` method.
-
-***
-
-## Generate Migration
-
-Instead of you writing the migration manually, the Medusa CLI tool provides a [db:generate](https://docs.medusajs.com/resources/medusa-cli/commands/db#dbgenerate/index.html.md) command to generate a migration for a modules' data models.
-
-For example, assuming you have a `blog` Module, you can generate a migration for it by running the following command:
-
-```bash
-npx medusa db:generate blog
-```
-
-This generates a migration file under the `migrations` directory of the Blog Module. You can then run it to reflect the changes in the database as mentioned in [this section](#run-the-migration).
-
-***
-
-## Write a Migration Manually
-
-You can also write migrations manually. To do that, create a file in the `migrations` directory of the module and in it, a class that has an `up` and `down` method. The class's name should be of the format `Migration{YEAR}{MONTH}{DAY}{HOUR}{MINUTE}.ts` to ensure migrations are ran in the correct order.
-
-For example:
-
-```ts title="src/modules/blog/migrations/Migration202507021059.ts"
-import { Migration } from "@mikro-orm/migrations"
-
-export class Migration202507021059 extends Migration {
-
- async up(): Promise {
- this.addSql("create table if not exists \"author\" (\"id\" text not null, \"name\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"author_pkey\" primary key (\"id\"));")
- }
-
- async down(): Promise {
- this.addSql("drop table if exists \"author\" cascade;")
- }
-
-}
-```
-
-The migration class in the file extends the `Migration` class imported from `@mikro-orm/migrations`. In the `up` and `down` method of the migration class, you use the `addSql` method provided by MikroORM's `Migration` class to run PostgreSQL syntax.
-
-In the example above, the `up` method creates the table `author`, and the `down` method drops the table if the migration is reverted.
-
-Refer to [MikroORM's documentation](https://mikro-orm.io/docs/migrations#migration-class) for more details on writing migrations.
-
-***
-
-## Run the Migration
-
-To run your migration, run the following command:
-
-This command also syncs module links. If you don't want that, use the `--skip-links` option.
-
-```bash
-npx medusa db:migrate
-```
-
-This reflects the changes in the database as implemented in the migration's `up` method.
-
-***
-
-## Rollback the Migration
-
-To rollback or revert the last migration you ran for a module, run the following command:
-
-```bash
-npx medusa db:rollback blog
-```
-
-This rolls back the last ran migration on the Blog Module.
-
-### Caution: Rollback Migration before Deleting
-
-If you need to delete a migration file, make sure to rollback the migration first. Otherwise, you might encounter issues when generating and running new migrations.
-
-For example, if you delete the migration of the Blog Module, then try to create a new one, Medusa will create a brand new migration that re-creates the tables or indices. If those are still in the database, you might encounter errors.
-
-So, always rollback the migration before deleting it.
-
-***
-
-## More Database Commands
-
-To learn more about the Medusa CLI's database commands, refer to [this CLI reference](https://docs.medusajs.com/resources/medusa-cli/commands/db/index.html.md).
-
-
-# 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.
-
-## Link Table's Default Columns
-
-When you define a link between two data models, Medusa creates a link table in the database to store the IDs of the linked records. You can learn more about the created table in the [Module Links chapter](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md).
-
-In various cases, you might need to store additional data in the link table. For example, if you define a link between a `product` and a `post`, you might want to store the publish date of the product's post in the link table.
-
-In those cases, you can add a custom column to a link's table in the link definition. You can later set that column whenever you create or update a link between the linked records.
-
-***
-
-## How to Add Custom Columns to a Link's Table?
-
-The `defineLink` function used to define a link accepts a third parameter, which is an object of options.
-
-To add custom columns to a link's table, pass in the third parameter of `defineLink` a `database` property:
-
-```ts highlights={linkHighlights}
-import BlogModule from "../modules/blog"
-import ProductModule from "@medusajs/medusa/product"
-import { defineLink } from "@medusajs/framework/utils"
-
-export default defineLink(
- ProductModule.linkable.product,
- BlogModule.linkable.blog,
- {
- database: {
- extraColumns: {
- metadata: {
- type: "json",
- },
- },
- },
- }
-)
-```
-
-This adds to the table created for the link between `product` and `blog` a `metadata` column of type `json`.
-
-### Database Options
-
-The `database` property defines configuration for the table created in the database.
-
-Its `extraColumns` property defines custom columns to create in the link's table.
-
-`extraColumns`'s value is an object whose keys are the names of the columns, and values are the column's configurations as an object.
-
-### Column Configurations
-
-The column's configurations object accepts the following properties:
-
-- `type`: The column's type. Possible values are:
- - `string`
- - `text`
- - `integer`
- - `boolean`
- - `date`
- - `time`
- - `datetime`
- - `enum`
- - `json`
- - `array`
- - `enumArray`
- - `float`
- - `double`
- - `decimal`
- - `bigint`
- - `mediumint`
- - `smallint`
- - `tinyint`
- - `blob`
- - `uuid`
- - `uint8array`
-- `defaultValue`: The column's default value.
-- `nullable`: Whether the column can have `null` values.
-
-***
-
-## Set Custom Column when Creating Link
-
-The object you pass to Link's `create` method accepts a `data` property. Its value is an object whose keys are custom column names, and values are the value of the custom column for this link.
-
-For example:
-
-Learn more about Link, how to resolve it, and its methods in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/link/index.html.md).
-
-```ts
-await link.create({
- [Modules.PRODUCT]: {
- product_id: "123",
- },
- [BLOG_MODULE]: {
- post_id: "321",
- },
- data: {
- metadata: {
- test: true,
- },
- },
-})
-```
-
-***
-
-## Retrieve Custom Column with Link
-
-To retrieve linked records with their custom columns, use [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). A module link's definition, exported by a file under `src/links`, has a special `entryPoint` property. Use this property when specifying the `entity` property in Query's `graph` method.
-
-For example:
-
-```ts highlights={retrieveHighlights}
-import productPostLink from "../links/product-post"
-
-// ...
-
-const { data } = await query.graph({
- entity: productPostLink.entryPoint,
- fields: ["metadata", "product.*", "post.*"],
- filters: {
- product_id: "prod_123",
- },
-})
-```
-
-This retrieves the product of id `prod_123` and its linked `post` records.
-
-In the `fields` array you pass `metadata`, which is the custom column to retrieve of the link.
-
-***
-
-## Update Custom Column's Value
-
-Link's `create` method updates a link's data if the link between the specified records already exists.
-
-So, to update the value of a custom column in a created link, use the `create` method again passing it a new value for the custom column.
-
-For example:
-
-```ts
-await link.create({
- [Modules.PRODUCT]: {
- product_id: "123",
- },
- [BLOG_MODULE]: {
- post_id: "321",
- },
- data: {
- metadata: {
- test: false,
- },
- },
-})
-```
-
-
-# Module Link Direction
-
-In this chapter, you'll learn about the difference in module link directions, and which to use based on your use case.
-
-The details in this chapter don't apply to [Read-Only Module Links](https://docs.medusajs.com/learn/fundamentals/module-links/read-only/index.html.md). Refer to the [Read-Only Module Links chapter](https://docs.medusajs.com/learn/fundamentals/module-links/read-only/index.html.md) for more information on read-only links and their direction.
-
-## Link Direction
-
-The module link's direction depends on the order you pass the data model configuration parameters to `defineLink`.
-
-For example, the following defines a link from the Blog Module's `post` data model to the Product Module's `product` data model:
-
-```ts
-export default defineLink(
- BlogModule.linkable.post,
- ProductModule.linkable.product
-)
-```
-
-Whereas the following defines a link from the Product Module's `product` data model to the Blog Module's `post` data model:
-
-```ts
-export default defineLink(
- ProductModule.linkable.product,
- BlogModule.linkable.post
-)
-```
-
-The above links are two different links that serve different purposes.
-
-***
-
-## Which Link Direction to Use?
-
-### Extend Data Models
-
-If you're adding a link to a data model to extend it and add new fields, define the link from the main data model to the custom data model.
-
-For example, consider you want to add a `subtitle` custom field to the `product` data model. To do that, you define a `Subtitle` data model in your module, then define a link from the `Product` data model to it:
-
-```ts
-export default defineLink(
- ProductModule.linkable.product,
- BlogModule.linkable.subtitle
-)
-```
-
-### Associate Data Models
-
-If you're linking data models to indicate an association between them, define the link from the custom data model to the main data model.
-
-For example, consider you have `Post` data model representing a blog post, and you want to associate a blog post with a product. To do that, define a link from the `Post` data model to `Product`:
-
-```ts
-export default defineLink(
- BlogModule.linkable.post,
- ProductModule.linkable.product
-)
-```
-
-
# Link
In this chapter, you’ll learn what Link is and how to use it to manage links.
@@ -12425,6 +12890,232 @@ Try passing one of the Query configuration parameters, like `fields` or `limit`,
Learn more about [specifing fields and relations](https://docs.medusajs.com/api/store#select-fields-and-relations) and [pagination](https://docs.medusajs.com/api/store#pagination) in the API reference.
+# Query Context
+
+In this chapter, you'll learn how to pass contexts when retrieving data with [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md).
+
+## What is Query Context?
+
+Query context is a way to pass additional information when retrieving data with Query. This data can be useful when applying custom transformations to the retrieved data based on the current context.
+
+For example, consider you have a Blog Module with posts and authors. You can accept the user's language as a context and return the posts in the user's language. Another example is how Medusa uses Query Context to [retrieve product variants' prices based on the customer's currency](https://docs.medusajs.com/resources/commerce-modules/product/guides/price/index.html.md).
+
+***
+
+## How to Use Query Context
+
+The `query.graph` method accepts an optional `context` parameter that can be used to pass additional context either to the data model you're retrieving (for example, `post`), or its related and linked models (for example, `author`).
+
+You initialize a context using `QueryContext` from the Modules SDK. It accepts an object of contexts as an argument.
+
+For example, to retrieve posts using Query while passing the user's language as a context:
+
+```ts
+const { data } = await query.graph({
+ entity: "post",
+ fields: ["*"],
+ context: QueryContext({
+ lang: "es",
+ }),
+})
+```
+
+In this example, you pass in the context a `lang` property whose value is `es`.
+
+Then, to handle the context while retrieving records of the data model, in the associated module's service you override the generated `list` method of the data model.
+
+For example, continuing the example above, you can override the `listPosts` method of the Blog Module's service to handle the context:
+
+```ts highlights={highlights2}
+import { MedusaContext, MedusaService } from "@medusajs/framework/utils"
+import { Context, FindConfig } from "@medusajs/framework/types"
+import Post from "./models/post"
+import Author from "./models/author"
+
+class BlogModuleService extends MedusaService({
+ Post,
+ Author,
+}){
+ // @ts-ignore
+ async listPosts(
+ filters?: any,
+ config?: FindConfig | undefined,
+ @MedusaContext() sharedContext?: Context | undefined
+ ) {
+ const context = filters.context ?? {}
+ delete filters.context
+
+ let posts = await super.listPosts(filters, config, sharedContext)
+
+ if (context.lang === "es") {
+ posts = posts.map((post) => {
+ return {
+ ...post,
+ title: post.title + " en español",
+ }
+ })
+ }
+
+ return posts
+ }
+}
+
+export default BlogModuleService
+```
+
+In the above example, you override the generated `listPosts` method. This method receives as a first parameter the filters passed to the query, but it also includes a `context` property that holds the context passed to the query.
+
+You extract the context from `filters`, then retrieve the posts using the parent's `listPosts` method. After that, if the language is set in the context, you transform the titles of the posts.
+
+All posts returned will now have their titles appended with "en español".
+
+Learn more about the generated `list` method in [this reference](https://docs.medusajs.com/resources/service-factory-reference/methods/list/index.html.md).
+
+### Using Pagination with Query
+
+If you pass pagination fields to `query.graph`, you must also override the `listAndCount` method in the service.
+
+For example, following along with the previous example, you must override the `listAndCountPosts` method of the Blog Module's service:
+
+```ts
+import { MedusaContext, MedusaService } from "@medusajs/framework/utils"
+import { Context, FindConfig } from "@medusajs/framework/types"
+import Post from "./models/post"
+import Author from "./models/author"
+
+class BlogModuleService extends MedusaService({
+ Post,
+ Author,
+}){
+ // @ts-ignore
+ async listAndCountPosts(
+ filters?: any,
+ config?: FindConfig | undefined,
+ @MedusaContext() sharedContext?: Context | undefined
+ ) {
+ const context = filters.context ?? {}
+ delete filters.context
+
+ const result = await super.listAndCountPosts(
+ filters,
+ config,
+ sharedContext
+ )
+
+ if (context.lang === "es") {
+ result.posts = posts.map((post) => {
+ return {
+ ...post,
+ title: post.title + " en español",
+ }
+ })
+ }
+
+ return result
+ }
+}
+
+export default BlogModuleService
+```
+
+Now, the `listAndCountPosts` method will handle the context passed to `query.graph` when you pass pagination fields. You can also move the logic to transform the posts' titles to a separate method and call it from both `listPosts` and `listAndCountPosts`.
+
+***
+
+## Passing Query Context to Related Data Models
+
+If you're retrieving a data model and you want to pass context to its associated model in the same module, you can pass them as part of `QueryContext`'s parameter, then handle them in the same `list` method.
+
+For linked data models, check out the [next section](#passing-query-context-to-linked-data-models).
+
+For example, to pass a context for the post's authors:
+
+```ts highlights={highlights3}
+const { data } = await query.graph({
+ entity: "post",
+ fields: ["*"],
+ context: QueryContext({
+ lang: "es",
+ author: QueryContext({
+ lang: "es",
+ }),
+ }),
+})
+```
+
+Then, in the `listPosts` method, you can handle the context for the post's authors:
+
+```ts highlights={highlights4}
+import { MedusaContext, MedusaService } from "@medusajs/framework/utils"
+import { Context, FindConfig } from "@medusajs/framework/types"
+import Post from "./models/post"
+import Author from "./models/author"
+
+class BlogModuleService extends MedusaService({
+ Post,
+ Author,
+}){
+ // @ts-ignore
+ async listPosts(
+ filters?: any,
+ config?: FindConfig | undefined,
+ @MedusaContext() sharedContext?: Context | undefined
+ ) {
+ const context = filters.context ?? {}
+ delete filters.context
+
+ let posts = await super.listPosts(filters, config, sharedContext)
+
+ const isPostLangEs = context.lang === "es"
+ const isAuthorLangEs = context.author?.lang === "es"
+
+ if (isPostLangEs || isAuthorLangEs) {
+ posts = posts.map((post) => {
+ return {
+ ...post,
+ title: isPostLangEs ? post.title + " en español" : post.title,
+ author: {
+ ...post.author,
+ name: isAuthorLangEs ? post.author.name + " en español" : post.author.name,
+ },
+ }
+ })
+ }
+
+ return posts
+ }
+}
+
+export default BlogModuleService
+```
+
+The context in `filters` will also have the context for `author`, which you can use to make transformations to the post's authors.
+
+***
+
+## Passing Query Context to Linked Data Models
+
+If you're retrieving a data model and you want to pass context to a linked model in a different module, pass to the `context` property an object instead, where its keys are the linked model's name and the values are the context for that linked model.
+
+For example, consider the Product Module's `Product` data model is linked to the Blog Module's `Post` data model. You can pass context to the `Post` data model while retrieving products like so:
+
+```ts highlights={highlights5}
+const { data } = await query.graph({
+ entity: "product",
+ fields: ["*", "post.*"],
+ context: {
+ post: QueryContext({
+ lang: "es",
+ }),
+ },
+})
+```
+
+In this example, you retrieve products and their associated posts. You also pass a context for `post`, indicating the customer's language.
+
+To handle the context, you override the generated `listPosts` method of the Blog Module as explained [previously](#how-to-use-query-context).
+
+
# Read-Only Module Link
In this chapter, you’ll learn what a read-only module link is and how to define one.
@@ -12931,666 +13622,6 @@ If multiple posts have their `product_id` set to a product's ID, an array of pos
[Sanity Integration Tutorial](https://docs.medusajs.com/resources/integrations/guides/sanity/index.html.md).
-# Query Context
-
-In this chapter, you'll learn how to pass contexts when retrieving data with [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md).
-
-## What is Query Context?
-
-Query context is a way to pass additional information when retrieving data with Query. This data can be useful when applying custom transformations to the retrieved data based on the current context.
-
-For example, consider you have a Blog Module with posts and authors. You can accept the user's language as a context and return the posts in the user's language. Another example is how Medusa uses Query Context to [retrieve product variants' prices based on the customer's currency](https://docs.medusajs.com/resources/commerce-modules/product/guides/price/index.html.md).
-
-***
-
-## How to Use Query Context
-
-The `query.graph` method accepts an optional `context` parameter that can be used to pass additional context either to the data model you're retrieving (for example, `post`), or its related and linked models (for example, `author`).
-
-You initialize a context using `QueryContext` from the Modules SDK. It accepts an object of contexts as an argument.
-
-For example, to retrieve posts using Query while passing the user's language as a context:
-
-```ts
-const { data } = await query.graph({
- entity: "post",
- fields: ["*"],
- context: QueryContext({
- lang: "es",
- }),
-})
-```
-
-In this example, you pass in the context a `lang` property whose value is `es`.
-
-Then, to handle the context while retrieving records of the data model, in the associated module's service you override the generated `list` method of the data model.
-
-For example, continuing the example above, you can override the `listPosts` method of the Blog Module's service to handle the context:
-
-```ts highlights={highlights2}
-import { MedusaContext, MedusaService } from "@medusajs/framework/utils"
-import { Context, FindConfig } from "@medusajs/framework/types"
-import Post from "./models/post"
-import Author from "./models/author"
-
-class BlogModuleService extends MedusaService({
- Post,
- Author,
-}){
- // @ts-ignore
- async listPosts(
- filters?: any,
- config?: FindConfig | undefined,
- @MedusaContext() sharedContext?: Context | undefined
- ) {
- const context = filters.context ?? {}
- delete filters.context
-
- let posts = await super.listPosts(filters, config, sharedContext)
-
- if (context.lang === "es") {
- posts = posts.map((post) => {
- return {
- ...post,
- title: post.title + " en español",
- }
- })
- }
-
- return posts
- }
-}
-
-export default BlogModuleService
-```
-
-In the above example, you override the generated `listPosts` method. This method receives as a first parameter the filters passed to the query, but it also includes a `context` property that holds the context passed to the query.
-
-You extract the context from `filters`, then retrieve the posts using the parent's `listPosts` method. After that, if the language is set in the context, you transform the titles of the posts.
-
-All posts returned will now have their titles appended with "en español".
-
-Learn more about the generated `list` method in [this reference](https://docs.medusajs.com/resources/service-factory-reference/methods/list/index.html.md).
-
-### Using Pagination with Query
-
-If you pass pagination fields to `query.graph`, you must also override the `listAndCount` method in the service.
-
-For example, following along with the previous example, you must override the `listAndCountPosts` method of the Blog Module's service:
-
-```ts
-import { MedusaContext, MedusaService } from "@medusajs/framework/utils"
-import { Context, FindConfig } from "@medusajs/framework/types"
-import Post from "./models/post"
-import Author from "./models/author"
-
-class BlogModuleService extends MedusaService({
- Post,
- Author,
-}){
- // @ts-ignore
- async listAndCountPosts(
- filters?: any,
- config?: FindConfig | undefined,
- @MedusaContext() sharedContext?: Context | undefined
- ) {
- const context = filters.context ?? {}
- delete filters.context
-
- const result = await super.listAndCountPosts(
- filters,
- config,
- sharedContext
- )
-
- if (context.lang === "es") {
- result.posts = posts.map((post) => {
- return {
- ...post,
- title: post.title + " en español",
- }
- })
- }
-
- return result
- }
-}
-
-export default BlogModuleService
-```
-
-Now, the `listAndCountPosts` method will handle the context passed to `query.graph` when you pass pagination fields. You can also move the logic to transform the posts' titles to a separate method and call it from both `listPosts` and `listAndCountPosts`.
-
-***
-
-## Passing Query Context to Related Data Models
-
-If you're retrieving a data model and you want to pass context to its associated model in the same module, you can pass them as part of `QueryContext`'s parameter, then handle them in the same `list` method.
-
-For linked data models, check out the [next section](#passing-query-context-to-linked-data-models).
-
-For example, to pass a context for the post's authors:
-
-```ts highlights={highlights3}
-const { data } = await query.graph({
- entity: "post",
- fields: ["*"],
- context: QueryContext({
- lang: "es",
- author: QueryContext({
- lang: "es",
- }),
- }),
-})
-```
-
-Then, in the `listPosts` method, you can handle the context for the post's authors:
-
-```ts highlights={highlights4}
-import { MedusaContext, MedusaService } from "@medusajs/framework/utils"
-import { Context, FindConfig } from "@medusajs/framework/types"
-import Post from "./models/post"
-import Author from "./models/author"
-
-class BlogModuleService extends MedusaService({
- Post,
- Author,
-}){
- // @ts-ignore
- async listPosts(
- filters?: any,
- config?: FindConfig | undefined,
- @MedusaContext() sharedContext?: Context | undefined
- ) {
- const context = filters.context ?? {}
- delete filters.context
-
- let posts = await super.listPosts(filters, config, sharedContext)
-
- const isPostLangEs = context.lang === "es"
- const isAuthorLangEs = context.author?.lang === "es"
-
- if (isPostLangEs || isAuthorLangEs) {
- posts = posts.map((post) => {
- return {
- ...post,
- title: isPostLangEs ? post.title + " en español" : post.title,
- author: {
- ...post.author,
- name: isAuthorLangEs ? post.author.name + " en español" : post.author.name,
- },
- }
- })
- }
-
- return posts
- }
-}
-
-export default BlogModuleService
-```
-
-The context in `filters` will also have the context for `author`, which you can use to make transformations to the post's authors.
-
-***
-
-## Passing Query Context to Linked Data Models
-
-If you're retrieving a data model and you want to pass context to a linked model in a different module, pass to the `context` property an object instead, where its keys are the linked model's name and the values are the context for that linked model.
-
-For example, consider the Product Module's `Product` data model is linked to the Blog Module's `Post` data model. You can pass context to the `Post` data model while retrieving products like so:
-
-```ts highlights={highlights5}
-const { data } = await query.graph({
- entity: "product",
- fields: ["*", "post.*"],
- context: {
- post: QueryContext({
- lang: "es",
- }),
- },
-})
-```
-
-In this example, you retrieve products and their associated posts. You also pass a context for `post`, indicating the customer's language.
-
-To handle the context, you override the generated `listPosts` method of the Blog Module as explained [previously](#how-to-use-query-context).
-
-
-# Create a Plugin
-
-In this chapter, you'll learn how to create a Medusa plugin and publish it.
-
-A [plugin](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md) is a package of reusable Medusa customizations that you can install in any Medusa application. By creating and publishing a plugin, you can reuse your Medusa customizations across multiple projects or share them with the community.
-
-Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0).
-
-## 1. Create a Plugin Project
-
-Plugins are created in a separate Medusa project. This makes the development and publishing of the plugin easier. Later, you'll install that plugin in your Medusa application to test it out and use it.
-
-Medusa's `create-medusa-app` CLI tool provides the option to create a plugin project. Run the following command to create a new plugin project:
-
-```bash
-npx create-medusa-app my-plugin --plugin
-```
-
-This will create a new Medusa plugin project in the `my-plugin` directory.
-
-### Plugin Directory Structure
-
-After the installation is done, the plugin structure will look like this:
-
-
-
-- `src/`: Contains the Medusa customizations.
-- `src/admin`: Contains [admin extensions](https://docs.medusajs.com/learn/fundamentals/admin/index.html.md).
-- `src/api`: Contains [API routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) and [middlewares](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md). You can add store, admin, or any custom API routes.
-- `src/jobs`: Contains [scheduled jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md).
-- `src/links`: Contains [module links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md).
-- `src/modules`: Contains [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md).
-- `src/provider`: Contains [module providers](#create-module-providers).
-- `src/subscribers`: Contains [subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md).
-- `src/workflows`: Contains [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). You can also add [hooks](https://docs.medusajs.com/learn/fundamentals/workflows/add-workflow-hook/index.html.md) under `src/workflows/hooks`.
-- `package.json`: Contains the plugin's package information, including general information and dependencies.
-- `tsconfig.json`: Contains the TypeScript configuration for the plugin.
-
-***
-
-## 2. Prepare Plugin
-
-### Package Name
-
-Before developing, testing, and publishing your plugin, make sure its name in `package.json` is correct. This is the name you'll use to install the plugin in your Medusa application.
-
-For example:
-
-```json title="package.json"
-{
- "name": "@myorg/plugin-name",
- // ...
-}
-```
-
-### Package Keywords
-
-Medusa scrapes NPM for a list of plugins that integrate third-party services, to later showcase them on the Medusa website. If you want your plugin to appear in that listing, make sure to add the `medusa-v2` and `medusa-plugin-integration` keywords to the `keywords` field in `package.json`.
-
-Only plugins that integrate third-party services are listed in the Medusa integrations page. If your plugin doesn't integrate a third-party service, you can skip this step.
-
-```json title="package.json"
-{
- "keywords": [
- "medusa-plugin-integration",
- "medusa-v2"
- ],
- // ...
-}
-```
-
-In addition, make sure to use one of the following keywords based on your integration type:
-
-|Keyword|Description|Example|
-|---|---|---|
-|\`medusa-plugin-analytics\`|Analytics service integration|Google Analytics|
-|\`medusa-plugin-auth\`|Authentication service integration|Auth0|
-|\`medusa-plugin-cms\`|CMS service integration|Contentful|
-|\`medusa-plugin-notification\`|Notification service integration|Twilio SMS|
-|\`medusa-plugin-payment\`|Payment service integration|PayPal|
-|\`medusa-plugin-search\`|Search service integration|MeiliSearch|
-|\`medusa-plugin-shipping\`|Shipping service integration|DHL|
-|\`medusa-plugin-other\`|Other third-party integrations|Sentry|
-
-### Package Dependencies
-
-Your plugin project will already have the dependencies mentioned in this section. Unless you made changes to the dependencies, you can skip this section.
-
-In the `package.json` file you must have the Medusa dependencies as `devDependencies` and `peerDependencies`. In addition, you must have `@swc/core` as a `devDependency`, as it's used by the plugin CLI tools.
-
-For example, assuming `2.5.0` is the latest Medusa version:
-
-```json title="package.json"
-{
- "devDependencies": {
- "@medusajs/admin-sdk": "2.5.0",
- "@medusajs/cli": "2.5.0",
- "@medusajs/framework": "2.5.0",
- "@medusajs/medusa": "2.5.0",
- "@medusajs/test-utils": "2.5.0",
- "@medusajs/ui": "4.0.4",
- "@medusajs/icons": "2.5.0",
- "@swc/core": "1.5.7",
- },
- "peerDependencies": {
- "@medusajs/admin-sdk": "2.5.0",
- "@medusajs/cli": "2.5.0",
- "@medusajs/framework": "2.5.0",
- "@medusajs/test-utils": "2.5.0",
- "@medusajs/medusa": "2.5.0",
- "@medusajs/ui": "4.0.3",
- "@medusajs/icons": "2.5.0",
- }
-}
-```
-
-### Package Exports
-
-Your plugin project will already have the exports mentioned in this section. Unless you made changes to the exports or you created your plugin before [Medusa v2.7.0](https://github.com/medusajs/medusa/releases/tag/v2.7.0), you can skip this section.
-
-In the `package.json` file, make sure your plugin has the following exports:
-
-```json title="package.json"
-{
- "exports": {
- "./package.json": "./package.json",
- "./workflows": "./.medusa/server/src/workflows/index.js",
- "./.medusa/server/src/modules/*": "./.medusa/server/src/modules/*/index.js",
- "./providers/*": "./.medusa/server/src/providers/*/index.js",
- "./admin": {
- "import": "./.medusa/server/src/admin/index.mjs",
- "require": "./.medusa/server/src/admin/index.js",
- "default": "./.medusa/server/src/admin/index.js"
- },
- "./*": "./.medusa/server/src/*.js"
- }
-}
-```
-
-Aside from the `./package.json`, `./providers`, and `./admin`, these exports are only a recommendation. You can cherry-pick the files and directories you want to export.
-
-The plugin exports the following files and directories:
-
-- `./package.json`: The `package.json` file. Medusa needs to access the `package.json` when registering the plugin.
-- `./workflows`: The workflows exported in `./src/workflows/index.ts`.
-- `./.medusa/server/src/modules/*`: The definition file of modules. This is useful if you create links to the plugin's modules in the Medusa application.
-- `./providers/*`: The definition file of module providers. This is useful if your plugin includes a module provider, allowing you to register the plugin's providers in Medusa applications. Learn more in the [Create Module Providers](#create-module-providers) section.
-- `./admin`: The admin extensions exported in `./src/admin/index.ts`.
-- `./*`: Any other files in the plugin's `src` directory.
-
-***
-
-## 3. Publish Plugin Locally for Development and Testing
-
-Medusa's CLI tool provides commands to simplify developing and testing your plugin in a local Medusa application. You start by publishing your plugin in the local package registry, then install it in your Medusa application. You can then watch for changes in the plugin as you develop it.
-
-### Publish and Install Local Package
-
-### Prerequisites
-
-- [Medusa application installed.](https://docs.medusajs.com/learn/installation/index.html.md)
-
-The first time you create your plugin, you need to publish the package into a local package registry, then install it in your Medusa application. This is a one-time only process.
-
-To publish the plugin to the local registry, run the following command in your plugin project:
-
-```bash title="Plugin project"
-npx medusa plugin:publish
-```
-
-This command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. The plugin is published locally under the name you specified in `package.json`.
-
-Next, navigate to your Medusa application:
-
-```bash title="Medusa application"
-cd ~/path/to/medusa-app
-```
-
-Make sure to replace `~/path/to/medusa-app` with the path to your Medusa application.
-
-Then, if your project was created before v2.3.1 of Medusa, make sure to install `yalc` as a development dependency:
-
-```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green"
-npm install --save-dev yalc
-```
-
-After that, run the following Medusa CLI command to install the plugin:
-
-```bash title="Medusa application"
-npx medusa plugin:add @myorg/plugin-name
-```
-
-Make sure to replace `@myorg/plugin-name` with the name of your plugin as specified in `package.json`. Your plugin will be installed from the local package registry into your Medusa application.
-
-### Register Plugin in Medusa Application
-
-After installing the plugin, you need to register it in your Medusa application in the configurations defined in `medusa-config.ts`.
-
-Add the plugin to the `plugins` array in the `medusa-config.ts` file:
-
-```ts title="medusa-config.ts" highlights={pluginHighlights}
-module.exports = defineConfig({
- // ...
- plugins: [
- {
- resolve: "@myorg/plugin-name",
- options: {},
- },
- ],
-})
-```
-
-The `plugins` configuration is an array of objects where each object has a `resolve` key whose value is the name of the plugin package.
-
-#### Pass Module Options through Plugin
-
-Each plugin configuration also accepts an `options` property, whose value is an object of options to pass to the plugin's modules.
-
-For example:
-
-```ts title="medusa-config.ts" highlights={pluginOptionsHighlight}
-module.exports = defineConfig({
- // ...
- plugins: [
- {
- resolve: "@myorg/plugin-name",
- options: {
- apiKey: true,
- },
- },
- ],
-})
-```
-
-The `options` property in the plugin configuration is passed to all modules in the plugin. Learn more about module options in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/options/index.html.md).
-
-### Watch Plugin Changes During Development
-
-While developing your plugin, you can watch for changes in the plugin and automatically update the plugin in the Medusa application using it. This is the only command you'll continuously need during your plugin development.
-
-To do that, run the following command in your plugin project:
-
-```bash title="Plugin project"
-npx medusa plugin:develop
-```
-
-This command will:
-
-- Watch for changes in the plugin. Whenever a file is changed, the plugin is automatically built.
-- Publish the plugin changes to the local package registry. This will automatically update the plugin in the Medusa application using it. You can also benefit from real-time HMR updates of admin extensions.
-
-### Start Medusa Application
-
-You can start your Medusa application's development server to test out your plugin:
-
-```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green"
-npm run dev
-```
-
-While your Medusa application is running and the plugin is being watched, you can test your plugin while developing it in the Medusa application.
-
-***
-
-## 4. Create Customizations in the Plugin
-
-You can now build your plugin's customizations. The following guide explains how to build different customizations in your plugin.
-
-- [Create a module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md)
-- [Create a module link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md)
-- [Create a workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md)
-- [Add a workflow hook](https://docs.medusajs.com/learn/fundamentals/workflows/add-workflow-hook/index.html.md)
-- [Create an API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md)
-- [Add a subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md)
-- [Add a scheduled job](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md)
-- [Add an admin widget](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md)
-- [Add an admin UI route](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md)
-
-While building those customizations, you can test them in your Medusa application by [watching the plugin changes](#watch-plugin-changes-during-development) and [starting the Medusa application](#start-medusa-application).
-
-### Generating Migrations for Modules
-
-During your development, you may need to generate migrations for modules in your plugin. To do that, first, add the following environment variables in your plugin project:
-
-```plain title="Plugin project"
-DB_USERNAME=postgres
-DB_PASSWORD=123...
-DB_HOST=localhost
-DB_PORT=5432
-DB_NAME=db_name
-```
-
-You can add these environment variables in a `.env` file in your plugin project. The variables are:
-
-- `DB_USERNAME`: The username of the PostgreSQL user to connect to the database.
-- `DB_PASSWORD`: The password of the PostgreSQL user to connect to the database.
-- `DB_HOST`: The host of the PostgreSQL database. Typically, it's `localhost` if you're running the database locally.
-- `DB_PORT`: The port of the PostgreSQL database. Typically, it's `5432` if you're running the database locally.
-- `DB_NAME`: The name of the PostgreSQL database to connect to.
-
-Then, run the following command in your plugin project to generate migrations for the modules in your plugin:
-
-```bash title="Plugin project"
-npx medusa plugin:db:generate
-```
-
-This command generates migrations for all modules in the plugin.
-
-Finally, run these migrations on the Medusa application that the plugin is installed in using the `db:migrate` command:
-
-```bash title="Medusa application"
-npx medusa db:migrate
-```
-
-The migrations in your application, including your plugin, will run and update the database.
-
-### Importing Module Resources
-
-In the [Prepare Plugin](#2-prepare-plugin) section, you learned about [exported resources](#package-exports) in your plugin.
-
-These exports allow you to import your plugin resources in your Medusa application, including workflows, links and modules.
-
-For example, to import the plugin's workflow in your Medusa application:
-
-`@myorg/plugin-name` is the plugin package's name.
-
-```ts
-import { Workflow1, Workflow2 } from "@myorg/plugin-name/workflows"
-import BlogModule from "@myorg/plugin-name/modules/blog"
-// import other files created in plugin like ./src/types/blog.ts
-import BlogType from "@myorg/plugin-name/types/blog"
-```
-
-### Create Module Providers
-
-The [exported resources](#package-exports) also allow you to import module providers in your plugin and register them in the Medusa application's configuration. A module provider is a module that provides the underlying logic or integration related to a commerce or Infrastructure Module.
-
-For example, assuming your plugin has a [Notification Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/notification/index.html.md) called `my-notification`, you can register it in your Medusa application's configuration like this:
-
-`@myorg/plugin-name` is the plugin package's name.
-
-```ts highlights={[["9"]]} title="medusa-config.ts"
-module.exports = defineConfig({
- // ...
- modules: [
- {
- resolve: "@medusajs/medusa/notification",
- options: {
- providers: [
- {
- resolve: "@myorg/plugin-name/providers/my-notification",
- id: "my-notification",
- options: {
- channels: ["email"],
- // provider options...
- },
- },
- ],
- },
- },
- ],
-})
-```
-
-You pass to `resolve` the path to the provider relative to the plugin package. So, in this example, the `my-notification` provider is located in `./src/providers/my-notification/index.ts` of the plugin.
-
-To learn how to create module providers, refer to the following guides:
-
-- [File Module Provider](https://docs.medusajs.com/resources/references/file-provider-module/index.html.md)
-- [Notification Module Provider](https://docs.medusajs.com/resources/references/notification-provider-module/index.html.md)
-- [Auth Module Provider](https://docs.medusajs.com/resources/references/auth/provider/index.html.md)
-- [Payment Module Provider](https://docs.medusajs.com/resources/references/payment/provider/index.html.md)
-- [Fulfillment Module Provider](https://docs.medusajs.com/resources/references/fulfillment/provider/index.html.md)
-- [Tax Module Provider](https://docs.medusajs.com/resources/references/tax/provider/index.html.md)
-
-***
-
-## 5. Publish Plugin to NPM
-
-Make sure to add the keywords mentioned in the [Package Keywords](#package-keywords) section in your plugin's `package.json` file.
-
-Medusa's CLI tool provides a command that bundles your plugin to be published to npm. Once you're ready to publish your plugin publicly, run the following command in your plugin project:
-
-```bash
-npx medusa plugin:build
-```
-
-The command will compile an output in the `.medusa/server` directory.
-
-You can now publish the plugin to npm using the [NPM CLI tool](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). Run the following command to publish the plugin to npm:
-
-```bash
-npm publish
-```
-
-If you haven't logged in before with your NPM account, you'll be asked to log in first. Then, your package is published publicly to be used in any Medusa application.
-
-### Install Public Plugin in Medusa Application
-
-You install a plugin that's published publicly using your package manager. For example:
-
-```bash npm2yarn
-npm install @myorg/plugin-name
-```
-
-Where `@myorg/plugin-name` is the name of your plugin as published on NPM.
-
-Then, register the plugin in your Medusa application's configurations as explained in [this section](#register-plugin-in-medusa-application).
-
-***
-
-## Update a Published Plugin
-
-To update the Medusa dependencies in a plugin, refer to [this documentation](https://docs.medusajs.com/learn/update#update-plugin-project/index.html.md).
-
-If you've published a plugin and you've made changes to it, you'll have to publish the update to NPM again.
-
-First, run the following command to change the version of the plugin:
-
-```bash
-npm version
-```
-
-Where `` indicates the type of version update you’re publishing. For example, it can be `major` or `minor`. Refer to the [npm version documentation](https://docs.npmjs.com/cli/v10/commands/npm-version) for more information.
-
-Then, re-run the same commands for publishing a plugin:
-
-```bash
-npx medusa plugin:build
-npm publish
-```
-
-This will publish an updated version of your plugin under a new version.
-
-
# Commerce Modules
In this chapter, you'll learn about Medusa's Commerce Modules.
@@ -14880,134 +14911,6 @@ The following directories are optional and their content are explained more in t
- `loaders`: Holds the scripts to run on the Medusa application's start-up.
-# Multiple Services in a Module
-
-In this chapter, you'll learn how to use multiple services in a module.
-
-## Module's Main and Internal Services
-
-A module has one main service only, which is the service exported in the module's definition.
-
-However, you may use other services in your module to better organize your code or split functionalities. These are called internal services that can be resolved within your module, but not in external resources.
-
-***
-
-## How to Add an Internal Service
-
-### 1. Create Service
-
-To add an internal service, create it in the `services` directory of your module.
-
-For example, create the file `src/modules/blog/services/client.ts` with the following content:
-
-```ts title="src/modules/blog/services/client.ts"
-export class ClientService {
- async getMessage(): Promise {
- return "Hello, World!"
- }
-}
-```
-
-### 2. Export Service in Index
-
-Next, create an `index.ts` file under the `services` directory of the module that exports your internal services.
-
-For example, create the file `src/modules/blog/services/index.ts` with the following content:
-
-```ts title="src/modules/blog/services/index.ts"
-export * from "./client"
-```
-
-This exports the `ClientService`.
-
-### 3. Resolve Internal Service
-
-Internal services exported in the `services/index.ts` file of your module are now registered in the container and can be resolved in other services in the module as well as loaders.
-
-For example, in your main service:
-
-```ts title="src/modules/blog/service.ts" highlights={[["5"], ["13"]]}
-// other imports...
-import { ClientService } from "./services"
-
-type InjectedDependencies = {
- clientService: ClientService
-}
-
-class BlogModuleService extends MedusaService({
- Post,
-}){
- protected clientService_: ClientService
-
- constructor({ clientService }: InjectedDependencies) {
- super(...arguments)
- this.clientService_ = clientService
- }
-}
-```
-
-You can now use your internal service in your main service.
-
-***
-
-## Resolve Resources in Internal Service
-
-Resolve dependencies from your module's container in the constructor of your internal service.
-
-For example:
-
-```ts
-import { Logger } from "@medusajs/framework/types"
-
-type InjectedDependencies = {
- logger: Logger
-}
-
-export class ClientService {
- protected logger_: Logger
-
- constructor({ logger }: InjectedDependencies) {
- this.logger_ = logger
- }
-}
-```
-
-***
-
-## Access Module Options
-
-Your internal service can't access the module's options.
-
-To retrieve the module's options, use the `configModule` registered in the module's container, which is the configurations in `medusa-config.ts`.
-
-For example:
-
-```ts
-import { ConfigModule } from "@medusajs/framework/types"
-import { BLOG_MODULE } from ".."
-
-export type InjectedDependencies = {
- configModule: ConfigModule
-}
-
-export class ClientService {
- protected options: Record
-
- constructor({ configModule }: InjectedDependencies) {
- const moduleDef = configModule.modules[BLOG_MODULE]
-
- if (typeof moduleDef !== "boolean") {
- this.options = moduleDef.options
- }
- }
-}
-```
-
-The `configModule` has a `modules` property that includes all registered modules. Retrieve the module's configuration using its registration key.
-
-If its value is not a `boolean`, set the service's options to the module configuration's `options` property.
-
-
# Module Options
In this chapter, you’ll learn about passing options to your module from the Medusa application’s configurations and using them in the module’s resources.
@@ -15173,6 +15076,134 @@ export default Module(BLOG_MODULE, {
Now, when the Medusa application starts, the loader will run, validating the module's options and throwing an error if the `apiKey` option is missing.
+# 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 Constraints
This chapter lists constraints to keep in mind when creating a service.
@@ -15386,484 +15417,6 @@ 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.
-
-
-# Translate Medusa Admin
-
-The Medusa Admin supports multiple languages, with the default being English. In this documentation, you'll learn how to contribute to the community by translating the Medusa Admin to a language you're fluent in.
-
-{/* vale docs.We = NO */}
-
-You can contribute either by translating the admin to a new language, or fixing translations for existing languages. As we can't validate every language's translations, some translations may be incorrect. Your contribution is welcome to fix any translation errors you find.
-
-{/* vale docs.We = YES */}
-
-Check out the translated languages either in the admin dashboard's settings or on [GitHub](https://github.com/medusajs/medusa/blob/develop/packages/admin/dashboard/src/i18n/languages.ts).
-
-***
-
-## How to Contribute Translation
-
-1. Clone the [Medusa monorepository](https://github.com/medusajs/medusa) to your local machine:
-
-```bash
-git clone https://github.com/medusajs/medusa.git
-```
-
-If you already have it cloned, make sure to pull the latest changes from the `develop` branch.
-
-2. Install the monorepository's dependencies. Since it's a Yarn workspace, it's highly recommended to use yarn:
-
-```bash
-yarn install
-```
-
-3. Create a branch that you'll use to open the pull request later:
-
-```bash
-git checkout -b feat/translate-
-```
-
-Where `` is your language name. For example, `feat/translate-da`.
-
-4. Translation files are under `packages/admin/dashboard/src/i18n/translations` as JSON files whose names are the ISO-2 name of the language.
- - If you're adding a new language, copy the file `packages/admin/dashboard/src/i18n/translations/en.json` and paste it with the ISO-2 name for your language. For example, if you're adding Danish translations, copy the `en.json` file and paste it as `packages/admin/dashboard/src/i18n/translations/de.json`.
- - If you're fixing a translation, find the JSON file of the language under `packages/admin/dashboard/src/i18n/translations`.
-
-5. Start translating the keys in the JSON file (or updating the targeted ones). All keys in the JSON file must be translated, and your PR tests will fail otherwise.
- - You can check whether the JSON file is valid by running the following command in `packages/admin/dashboard`, replacing `da.json` with the JSON file's name:
-
-```bash title="packages/admin/dashboard"
-yarn i18n:validate da.json
-```
-
-6. After finishing the translation, if you're adding a new language, import its JSON file in `packages/admin/dashboard/src/i18n/translations/index.ts` and add it to the exported object:
-
-```ts title="packages/admin/dashboard/src/i18n/translations/index.ts" highlights={[["2"], ["6"], ["7"], ["8"]]}
-// other imports...
-import da from "./da.json"
-
-export default {
- // other languages...
- da: {
- translation: da,
- },
-}
-```
-
-The language's key in the object is the ISO-2 name of the language.
-
-7. If you're adding a new language, add it to the file `packages/admin/dashboard/src/i18n/languages.ts`:
-
-```ts title="packages/admin/dashboard/src/i18n/languages.ts" highlights={languageHighlights}
-import { da } from "date-fns/locale"
-// other imports...
-
-export const languages: Language[] = [
- // other languages...
- {
- code: "da",
- display_name: "Danish",
- ltr: true,
- date_locale: da,
- },
-]
-```
-
-`languages` is an array having the following properties:
-
-- `code`: The ISO-2 name of the language. For example, `da` for Danish.
-- `display_name`: The language's name to be displayed in the admin.
-- `ltr`: Whether the language supports a left-to-right layout. For example, set this to `false` for languages like Arabic.
-- `date_locale`: An instance of the locale imported from the [date-fns/locale](https://date-fns.org/) package.
-
-8. Once you're done, push the changes into your branch and open a pull request on GitHub.
-
-Our team will perform a general review on your PR and merge it if no issues are found. The translation will be available in the admin after the next release.
-
-
-# Docs Contribution Guidelines
-
-Thank you for your interest in contributing to the documentation! You will be helping the open source community and other developers interested in learning more about Medusa and using it.
-
-This guide is specific to contributing to the documentation. If you’re interested in contributing to Medusa’s codebase, check out the [contributing guidelines in the Medusa GitHub repository](https://github.com/medusajs/medusa/blob/develop/CONTRIBUTING.md).
-
-## What Can You Contribute?
-
-You can contribute to the Medusa documentation in the following ways:
-
-- Fixes to existing content. This includes small fixes like typos, or adding missing information.
-- Additions to the documentation. If you think a documentation page can be useful to other developers, you can contribute by adding it.
- - Make sure to open an issue first in the [medusa repository](https://github.com/medusajs/medusa) to confirm that you can add that documentation page.
-- Fixes to UI components and tooling. If you find a bug while browsing the documentation, you can contribute by fixing it.
-
-***
-
-## Documentation Workspace
-
-Medusa's documentation projects are all part of the documentation `yarn` workspace, which you can find in the [medusa repository](https://github.com/medusajs/medusa) under the `www` directory.
-
-The workspace has the following two directories:
-
-- `apps`: this directory holds the different documentation websites and projects.
- - `book`: includes the codebase for the [main Medusa documentation](https://docs.medusajs.com//index.html.md). It's built with [Next.js 15](https://nextjs.org/).
- - `resources`: includes the codebase for the resources documentation, which powers different sections of the docs such as the [Integrations](https://docs.medusajs.com/resources/integrations/index.html.md) or [How-to & Tutorials](https://docs.medusajs.com/resources/how-to-tutorials/index.html.md) sections. It's built with [Next.js 15](https://nextjs.org/).
- - `api-reference`: includes the codebase for the API reference website. It's built with [Next.js 15](https://nextjs.org/).
- - `ui`: includes the codebase for the Medusa UI documentation website. It's built with [Next.js 15](https://nextjs.org/).
-- `packages`: this directory holds the shared packages and components necessary for the development of the projects in the `apps` directory.
- - `docs-ui` includes the shared React components between the different apps.
- - `remark-rehype-plugins` includes Remark and Rehype plugins used by the documentation projects.
-
-### Setup the Documentation Workspace
-
-### Prerequisites
-
-- [Node.js v20+](https://nodejs.org/en/download)
-- [Yarn v3+](https://v3.yarnpkg.com/getting-started/install)
-
-In the `www` directory, run the following command to install the dependencies:
-
-```bash
-yarn install
-```
-
-Then, run the following command to build packages under the `www/packages` directory:
-
-```bash
-yarn build
-```
-
-After that, you can change into the directory of any documentation project under the `www/apps` directory and run the `dev` command to start the development server.
-
-***
-
-## Documentation Content
-
-All documentation projects are built with Next.js. The content is writtin in MDX files.
-
-### Medusa Main Docs Content
-
-The content of the Medusa main docs are under the `www/apps/book/app` directory.
-
-### Medusa Resources Content
-
-The content of all pages under the `/resources` path are under the `www/apps/resources/app` directory.
-
-Documentation pages under the `www/apps/resources/references` directory are generated automatically from the source code under the `packages/medusa` directory. So, you can't directly make changes to them. Instead, you'll have to make changes to the comments in the original source code.
-
-### API Reference
-
-The API reference's content is split into two types:
-
-1. Static content, which are the content related to getting started, expanding fields, and more. These are located in the `www/apps/api-reference/markdown` directory. They are MDX files.
-2. OpenAPI specs that are shown to developers when checking the reference of an API Route. These are generated from OpenApi Spec comments, which are under the `www/utils/generated/oas-output` directory.
-
-### Medusa UI Documentation
-
-The content of the Medusa UI documentation are located under the `www/apps/ui/src/content/docs` directory. They are MDX files.
-
-The UI documentation also shows code examples, which are under the `www/apps/ui/src/examples` directory.
-
-The UI component props are generated from the source code and placed into the `www/apps/ui/src/specs` directory. To contribute to these props and their comments, check the comments in the source code under the `packages/design-system/ui` directory.
-
-***
-
-## Style Guide
-
-When you contribute to the documentation content, make sure to follow the [documentation style guide](https://www.notion.so/Style-Guide-Docs-fad86dd1c5f84b48b145e959f36628e0).
-
-***
-
-## How to Contribute
-
-If you’re fixing errors in an existing documentation page, you can scroll down to the end of the page and click on the “Edit this page” link. You’ll be redirected to the GitHub edit form of that page and you can make edits directly and submit a pull request (PR).
-
-If you’re adding a new page or contributing to the codebase, fork the repository, create a new branch, and make all changes necessary in your repository. Then, once you’re done, create a PR in the Medusa repository.
-
-### Base Branch
-
-When you make an edit to an existing documentation page or fork the repository to make changes to the documentation, create a new branch.
-
-Documentation contributions always use `develop` as the base branch. Make sure to also open your PR against the `develop` branch.
-
-### Branch Name
-
-Make sure that the branch name starts with `docs/`. For example, `docs/fix-services`. Vercel deployed previews are only triggered for branches starting with `docs/`.
-
-### Pull Request Conventions
-
-When you create a pull request, prefix the title with `docs:` or `docs(PROJECT_NAME):`, where `PROJECT_NAME` is the name of the documentation project this pull request pertains to. For example, `docs(ui): fix titles`.
-
-In the body of the PR, explain clearly what the PR does. If the PR solves an issue, use [closing keywords](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) with the issue number. For example, “Closes #1333”.
-
-***
-
-## Images
-
-If you are adding images to a documentation page, you can host the image on [Imgur](https://imgur.com) for free to include it in the PR. Our team will later upload it to our image hosting.
-
-***
-
-## NPM and Yarn Code Blocks
-
-If you’re adding code blocks that use NPM and Yarn, you must add the `npm2yarn` meta field.
-
-For example:
-
-````md
-```bash npm2yarn
-npm run start
-```
-````
-
-The code snippet must be written using NPM.
-
-### Global Option
-
-When a command uses the global option `-g`, add it at the end of the NPM command to ensure that it’s transformed to a Yarn command properly. For example:
-
-```bash npm2yarn
-npm install @medusajs/cli -g
-```
-
-***
-
-## Linting with Vale
-
-Medusa uses [Vale](https://vale.sh/) to lint documentation pages and perform checks on incoming PRs into the repository.
-
-### Result of Vale PR Checks
-
-You can check the result of running the "lint" action on your PR by clicking the Details link next to it. You can find there all errors that you need to fix.
-
-### Run Vale Locally
-
-If you want to check your work locally, you can do that by:
-
-1. [Installing Vale](https://vale.sh/docs/vale-cli/installation/) on your machine.
-2. Changing to the `www/vale` directory:
-
-```bash
-cd www/vale
-```
-
-3\. Running the `run-vale` script:
-
-```bash
-# to lint content for the main documentation
-./run-vale.sh book/app/learn error resources
-# to lint content for the resources documentation
-./run-vale.sh resources/app error
-# to lint content for the API reference
-./run-vale.sh api-reference/markdown error
-# to lint content for the Medusa UI documentation
-./run-vale.sh ui/src/content/docs error
-# to lint content for the user guide
-./run-vale.sh user-guide/app error
-```
-
-{/* TODO need to enable MDX v1 comments first. */}
-
-{/* ### Linter Exceptions
-
-If it's needed to break some style guide rules in a document, you can wrap the parts that the linter shouldn't scan with the following comments in the `md` or `mdx` files:
-
-```md
-
-
-content that shouldn't be scanned for errors here...
-
-
-```
-
-You can also disable specific rules. For example:
-
-```md
-
-
-Medusa supports Node versions 14 and 16.
-
-
-```
-
-If you use this in your PR, you must justify its usage. */}
-
-***
-
-## Linting with ESLint
-
-Medusa uses ESlint to lint code blocks both in the content and the code base of the documentation apps.
-
-### Linting Content with ESLint
-
-Each PR runs through a check that lints the code in the content files using ESLint. The action's name is `content-eslint`.
-
-If you want to check content ESLint errors locally and fix them, you can do that by:
-
-1\. Install the dependencies in the `www` directory:
-
-```bash
-yarn install
-```
-
-2\. Run the turbo command in the `www` directory:
-
-```bash
-turbo run lint:content
-```
-
-This will fix any fixable errors, and show errors that require your action.
-
-### Linting Code with ESLint
-
-Each PR runs through a check that lints the code in the content files using ESLint. The action's name is `code-docs-eslint`.
-
-If you want to check code ESLint errors locally and fix them, you can do that by:
-
-1\. Install the dependencies in the `www` directory:
-
-```bash
-yarn install
-```
-
-2\. Run the turbo command in the `www` directory:
-
-```bash
-yarn lint
-```
-
-This will fix any fixable errors, and show errors that require your action.
-
-{/* TODO need to enable MDX v1 comments first. */}
-
-{/* ### ESLint Exceptions
-
-If some code blocks have errors that can't or shouldn't be fixed, you can add the following command before the code block:
-
-~~~md
-
-
-```js
-console.log("This block isn't linted")
-```
-
-```js
-console.log("This block is linted")
-```
-~~~
-
-You can also disable specific rules. For example:
-
-~~~md
-
-
-```js
-console.log("This block can use semicolons");
-```
-
-```js
-console.log("This block can't use semi colons")
-```
-~~~ */}
-
-
-# Expose a Workflow Hook
-
-In this chapter, you'll learn how to expose a hook in your workflow.
-
-## When to Expose a Hook
-
-Your workflow is reusable in other applications, and you allow performing an external action at some point in your workflow.
-
-Your workflow isn't reusable by other applications. Use a step that performs what a hook handler would instead.
-
-***
-
-## How to Expose a Hook in a Workflow?
-
-To expose a hook in your workflow, use `createHook` from the Workflows SDK.
-
-For example:
-
-```ts title="src/workflows/my-workflow/index.ts" highlights={hookHighlights}
-import {
- createStep,
- createHook,
- createWorkflow,
- WorkflowResponse,
-} from "@medusajs/framework/workflows-sdk"
-import { createProductStep } from "./steps/create-product"
-
-export const myWorkflow = createWorkflow(
- "my-workflow",
- function (input) {
- const product = createProductStep(input)
- const productCreatedHook = createHook(
- "productCreated",
- { productId: product.id }
- )
-
- return new WorkflowResponse(product, {
- hooks: [productCreatedHook],
- })
- }
-)
-```
-
-The `createHook` function accepts two parameters:
-
-1. The first is a string indicating the hook's name. You use this to consume the hook later.
-2. The second is the input to pass to the hook handler.
-
-The workflow must also pass an object having a `hooks` property as a second parameter to the `WorkflowResponse` constructor. Its value is an array of the workflow's hooks.
-
-### How to Consume the Hook?
-
-To consume the hook of the workflow, create the file `src/workflows/hooks/my-workflow.ts` with the following content:
-
-```ts title="src/workflows/hooks/my-workflow.ts" highlights={handlerHighlights}
-import { myWorkflow } from "../my-workflow"
-
-myWorkflow.hooks.productCreated(
- async ({ productId }, { container }) => {
- // TODO perform an action
- }
-)
-```
-
-The hook is available on the workflow's `hooks` property using its name `productCreated`.
-
-You invoke the hook, passing a step function (the hook handler) as a parameter.
-
-
# Compensation Function
In this chapter, you'll learn what a compensation function is and how to add it to a step.
@@ -16120,6 +15673,105 @@ So, if an error occurs during the loop, the compensation function will still rec
For more details on error handling in workflows and steps, check the [Handling Errors chapter](https://docs.medusajs.com/learn/fundamentals/workflows/errors/index.html.md).
+# Scheduled Jobs Number of Executions
+
+In this chapter, you'll learn how to set a limit on the number of times a scheduled job is executed.
+
+## numberOfExecutions Option
+
+The export configuration object of the scheduled job accepts an optional property `numberOfExecutions`. Its value is a number indicating how many times the scheduled job can be executed during the Medusa application's runtime.
+
+For example:
+
+```ts highlights={highlights}
+export default async function myCustomJob() {
+ console.log("I'll be executed three times only.")
+}
+
+export const config = {
+ name: "hello-world",
+ // execute every minute
+ schedule: "* * * * *",
+ numberOfExecutions: 3,
+}
+```
+
+The above scheduled job has the `numberOfExecutions` configuration set to `3`.
+
+So, it'll only execute 3 times, each every minute, then it won't be executed anymore.
+
+If you restart the Medusa application, the scheduled job will be executed again until reaching the number of executions specified.
+
+
+# Expose a Workflow Hook
+
+In this chapter, you'll learn how to expose a hook in your workflow.
+
+## When to Expose a Hook
+
+Your workflow is reusable in other applications, and you allow performing an external action at some point in your workflow.
+
+Your workflow isn't reusable by other applications. Use a step that performs what a hook handler would instead.
+
+***
+
+## How to Expose a Hook in a Workflow?
+
+To expose a hook in your workflow, use `createHook` from the Workflows SDK.
+
+For example:
+
+```ts title="src/workflows/my-workflow/index.ts" highlights={hookHighlights}
+import {
+ createStep,
+ createHook,
+ createWorkflow,
+ WorkflowResponse,
+} from "@medusajs/framework/workflows-sdk"
+import { createProductStep } from "./steps/create-product"
+
+export const myWorkflow = createWorkflow(
+ "my-workflow",
+ function (input) {
+ const product = createProductStep(input)
+ const productCreatedHook = createHook(
+ "productCreated",
+ { productId: product.id }
+ )
+
+ return new WorkflowResponse(product, {
+ hooks: [productCreatedHook],
+ })
+ }
+)
+```
+
+The `createHook` function accepts two parameters:
+
+1. The first is a string indicating the hook's name. You use this to consume the hook later.
+2. The second is the input to pass to the hook handler.
+
+The workflow must also pass an object having a `hooks` property as a second parameter to the `WorkflowResponse` constructor. Its value is an array of the workflow's hooks.
+
+### How to Consume the Hook?
+
+To consume the hook of the workflow, create the file `src/workflows/hooks/my-workflow.ts` with the following content:
+
+```ts title="src/workflows/hooks/my-workflow.ts" highlights={handlerHighlights}
+import { myWorkflow } from "../my-workflow"
+
+myWorkflow.hooks.productCreated(
+ async ({ productId }, { container }) => {
+ // TODO perform an action
+ }
+)
+```
+
+The hook is available on the workflow's `hooks` property using its name `productCreated`.
+
+You invoke the hook, passing a step function (the hook handler) as a parameter.
+
+
# Conditions in Workflows with When-Then
In this chapter, you'll learn how to execute an action based on a condition in a workflow using when-then from the Workflows SDK.
@@ -17008,133 +16660,6 @@ const workflow = createWorkflow(
In this example, you use when-then to run the `createProductsWorkflow` only if `should_create` (passed in the `input`) is enabled.
-# Multiple Step Usage in Workflow
-
-In this chapter, you'll learn how to use a step multiple times in a workflow.
-
-## Problem Reusing a Step in a Workflow
-
-In some cases, you may need to use a step multiple times in the same workflow.
-
-The most common example is using the `useQueryGraphStep` multiple times in a workflow to retrieve multiple unrelated data, such as customers and products.
-
-Each workflow step must have a unique ID, which is the ID passed as a first parameter when creating the step:
-
-```ts
-const useQueryGraphStep = createStep(
- "use-query-graph"
- // ...
-)
-```
-
-This causes an error when you use the same step multiple times in a workflow, as it's registered in the workflow as two steps having the same ID:
-
-```ts
-const helloWorkflow = createWorkflow(
- "hello",
- () => {
- const { data: products } = useQueryGraphStep({
- entity: "product",
- fields: ["id"],
- })
-
- // ERROR OCCURS HERE: A STEP HAS THE SAME ID AS ANOTHER IN THE WORKFLOW
- const { data: customers } = useQueryGraphStep({
- entity: "customer",
- fields: ["id"],
- })
- }
-)
-```
-
-The next section explains how to fix this issue to use the same step multiple times in a workflow.
-
-***
-
-## How to Use a Step Multiple Times in a Workflow?
-
-When you execute a step in a workflow, you can chain a `config` method to it to change the step's config.
-
-Use the `config` method to change a step's ID for a single execution.
-
-So, this is the correct way to write the example above:
-
-```ts highlights={highlights}
-const helloWorkflow = createWorkflow(
- "hello",
- () => {
- const { data: products } = useQueryGraphStep({
- entity: "product",
- fields: ["id"],
- })
-
- // ✓ No error occurs, the step has a different ID.
- const { data: customers } = useQueryGraphStep({
- entity: "customer",
- fields: ["id"],
- }).config({ name: "fetch-customers" })
- }
-)
-```
-
-The `config` method accepts an object with a `name` property. Its value is a new ID of the step to use for this execution only.
-
-The first `useQueryGraphStep` usage has the ID `use-query-graph`, and the second `useQueryGraphStep` usage has the ID `fetch-customers`.
-
-
-# Run Workflow Steps in Parallel
-
-In this chapter, you’ll learn how to run workflow steps in parallel.
-
-## parallelize Utility Function
-
-If your workflow has steps that don’t rely on one another’s results, run them in parallel using `parallelize` from the Workflows SDK.
-
-The workflow waits until all steps passed to the `parallelize` function finish executing before continuing to the next step.
-
-For example:
-
-```ts highlights={highlights} collapsibleLines="1-12" expandButtonLabel="Show Imports"
-import {
- createWorkflow,
- WorkflowResponse,
- parallelize,
-} from "@medusajs/framework/workflows-sdk"
-import {
- createProductStep,
- getProductStep,
- createPricesStep,
- attachProductToSalesChannelStep,
-} from "./steps"
-
-interface WorkflowInput {
- title: string
-}
-
-const myWorkflow = createWorkflow(
- "my-workflow",
- (input: WorkflowInput) => {
- const product = createProductStep(input)
-
- const [prices, productSalesChannel] = parallelize(
- createPricesStep(product),
- attachProductToSalesChannelStep(product)
- )
-
- const refetchedProduct = getProductStep(product.id)
-
- return new WorkflowResponse(refetchedProduct)
- }
-)
-```
-
-The `parallelize` function accepts the steps to run in parallel as a parameter.
-
-It returns an array of the steps' results in the same order they're passed to the `parallelize` function.
-
-So, `prices` is the result of `createPricesStep`, and `productSalesChannel` is the result of `attachProductToSalesChannelStep`.
-
-
# Long-Running Workflows
In this chapter, you’ll learn what a long-running workflow is and how to configure it.
@@ -17430,6 +16955,133 @@ To find a full example of a long-running workflow, refer to the [restaurant-deli
In the recipe, you use a long-running workflow that moves an order from placed to completed. The workflow waits for the restaurant to accept the order, the driver to pick up the order, and other external actions.
+# Run Workflow Steps in Parallel
+
+In this chapter, you’ll learn how to run workflow steps in parallel.
+
+## parallelize Utility Function
+
+If your workflow has steps that don’t rely on one another’s results, run them in parallel using `parallelize` from the Workflows SDK.
+
+The workflow waits until all steps passed to the `parallelize` function finish executing before continuing to the next step.
+
+For example:
+
+```ts highlights={highlights} collapsibleLines="1-12" expandButtonLabel="Show Imports"
+import {
+ createWorkflow,
+ WorkflowResponse,
+ parallelize,
+} from "@medusajs/framework/workflows-sdk"
+import {
+ createProductStep,
+ getProductStep,
+ createPricesStep,
+ attachProductToSalesChannelStep,
+} from "./steps"
+
+interface WorkflowInput {
+ title: string
+}
+
+const myWorkflow = createWorkflow(
+ "my-workflow",
+ (input: WorkflowInput) => {
+ const product = createProductStep(input)
+
+ const [prices, productSalesChannel] = parallelize(
+ createPricesStep(product),
+ attachProductToSalesChannelStep(product)
+ )
+
+ const refetchedProduct = getProductStep(product.id)
+
+ return new WorkflowResponse(refetchedProduct)
+ }
+)
+```
+
+The `parallelize` function accepts the steps to run in parallel as a parameter.
+
+It returns an array of the steps' results in the same order they're passed to the `parallelize` function.
+
+So, `prices` is the result of `createPricesStep`, and `productSalesChannel` is the result of `attachProductToSalesChannelStep`.
+
+
+# Multiple Step Usage in Workflow
+
+In this chapter, you'll learn how to use a step multiple times in a workflow.
+
+## Problem Reusing a Step in a Workflow
+
+In some cases, you may need to use a step multiple times in the same workflow.
+
+The most common example is using the `useQueryGraphStep` multiple times in a workflow to retrieve multiple unrelated data, such as customers and products.
+
+Each workflow step must have a unique ID, which is the ID passed as a first parameter when creating the step:
+
+```ts
+const useQueryGraphStep = createStep(
+ "use-query-graph"
+ // ...
+)
+```
+
+This causes an error when you use the same step multiple times in a workflow, as it's registered in the workflow as two steps having the same ID:
+
+```ts
+const helloWorkflow = createWorkflow(
+ "hello",
+ () => {
+ const { data: products } = useQueryGraphStep({
+ entity: "product",
+ fields: ["id"],
+ })
+
+ // ERROR OCCURS HERE: A STEP HAS THE SAME ID AS ANOTHER IN THE WORKFLOW
+ const { data: customers } = useQueryGraphStep({
+ entity: "customer",
+ fields: ["id"],
+ })
+ }
+)
+```
+
+The next section explains how to fix this issue to use the same step multiple times in a workflow.
+
+***
+
+## How to Use a Step Multiple Times in a Workflow?
+
+When you execute a step in a workflow, you can chain a `config` method to it to change the step's config.
+
+Use the `config` method to change a step's ID for a single execution.
+
+So, this is the correct way to write the example above:
+
+```ts highlights={highlights}
+const helloWorkflow = createWorkflow(
+ "hello",
+ () => {
+ const { data: products } = useQueryGraphStep({
+ entity: "product",
+ fields: ["id"],
+ })
+
+ // ✓ No error occurs, the step has a different ID.
+ const { data: customers } = useQueryGraphStep({
+ entity: "customer",
+ fields: ["id"],
+ }).config({ name: "fetch-customers" })
+ }
+)
+```
+
+The `config` method accepts an object with a `name` property. Its value is a new ID of the step to use for this execution only.
+
+The first `useQueryGraphStep` usage has the ID `use-query-graph`, and the second `useQueryGraphStep` usage has the ID `fetch-customers`.
+
+
# Retry Failed Steps
In this chapter, you’ll learn how to configure steps to allow retrial on failure.
@@ -17553,130 +17205,6 @@ However, since the long-running workflow runs in the background, you won't recei
Instead, you must subscribe to the workflow's execution using the Workflow Engine Module Service. Learn more about it in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/long-running-workflow#access-long-running-workflow-status-and-result/index.html.md).
-# Workflow Hooks
-
-In this chapter, you'll learn what a workflow hook is and how to consume them.
-
-## What is a Workflow Hook?
-
-A workflow hook is a point in a workflow where you can inject custom functionality as a step function, called a hook handler.
-
-Medusa exposes hooks in many of its workflows that are used in its API routes. You can consume those hooks to add your custom logic.
-
-Refer to the [Workflows Reference](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md) to view all workflows and their hooks.
-
-You want to perform a custom action during a workflow's execution, such as when a product is created.
-
-***
-
-## How to Consume a Hook?
-
-A workflow has a special `hooks` property which is an object that holds its hooks.
-
-So, in a TypeScript or JavaScript file created under the `src/workflows/hooks` directory:
-
-- Import the workflow.
-- Access its hook using the `hooks` property.
-- Pass the hook a step function as a parameter to consume it.
-
-For example, to consume the `productsCreated` hook of Medusa's `createProductsWorkflow`, create the file `src/workflows/hooks/product-created.ts` with the following content:
-
-```ts title="src/workflows/hooks/product-created.ts" highlights={handlerHighlights}
-import { createProductsWorkflow } from "@medusajs/medusa/core-flows"
-
-createProductsWorkflow.hooks.productsCreated(
- async ({ products }, { container }) => {
- // TODO perform an action
- }
-)
-```
-
-The `productsCreated` hook is available on the workflow's `hooks` property by its name.
-
-You invoke the hook, passing a step function (the hook handler) as a parameter.
-
-Now, when a product is created using the [Create Product API route](https://docs.medusajs.com/api/admin#products_postproducts), your hook handler is executed after the product is created.
-
-A hook can have only one handler.
-
-Refer to the [createProductsWorkflow reference](https://docs.medusajs.com/resources/references/medusa-workflows/createProductsWorkflow/index.html.md) to see at which point the hook handler is executed.
-
-### Hook Handler Parameter
-
-Since a hook handler is essentially a step function, it receives the hook's input as a first parameter, and an object holding a `container` property as a second parameter.
-
-Each hook has different input. For example, the `productsCreated` hook receives an object having a `products` property holding the created product.
-
-### Hook Handler Compensation
-
-Since the hook handler is a step function, you can set its compensation function as a second parameter of the hook.
-
-For example:
-
-```ts title="src/workflows/hooks/product-created.ts"
-import { createProductsWorkflow } from "@medusajs/medusa/core-flows"
-
-createProductsWorkflow.hooks.productsCreated(
- async ({ products }, { container }) => {
- // TODO perform an action
-
- return new StepResponse(undefined, { ids })
- },
- async ({ ids }, { container }) => {
- // undo the performed action
- }
-)
-```
-
-The compensation function is executed if an error occurs in the workflow to undo the actions performed by the hook handler.
-
-The compensation function receives as an input the second parameter passed to the `StepResponse` returned by the step function.
-
-It also accepts as a second parameter an object holding a `container` property to resolve resources from the Medusa container.
-
-### Additional Data Property
-
-Medusa's workflows pass in the hook's input an `additional_data` property:
-
-```ts title="src/workflows/hooks/product-created.ts" highlights={[["4", "additional_data"]]}
-import { createProductsWorkflow } from "@medusajs/medusa/core-flows"
-
-createProductsWorkflow.hooks.productsCreated(
- async ({ products, additional_data }, { container }) => {
- // TODO perform an action
- }
-)
-```
-
-This property is an object that holds additional data passed to the workflow through the request sent to the API route using the workflow.
-
-Learn how to pass `additional_data` in requests to API routes in [this chapter](https://docs.medusajs.com/learn/fundamentals/api-routes/additional-data/index.html.md).
-
-### Pass Additional Data to Workflow
-
-You can also pass that additional data when executing the workflow. Pass it as a parameter to the `.run` method of the workflow:
-
-```ts title="src/workflows/hooks/product-created.ts" highlights={[["10", "additional_data"]]}
-import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
-import { createProductsWorkflow } from "@medusajs/medusa/core-flows"
-
-export async function POST(req: MedusaRequest, res: MedusaResponse) {
- await createProductsWorkflow(req.scope).run({
- input: {
- products: [
- // ...
- ],
- additional_data: {
- custom_field: "test",
- },
- },
- })
-}
-```
-
-Your hook handler then receives that passed data in the `additional_data` object.
-
-
# Store Workflow Executions
In this chapter, you'll learn how to store workflow executions in the database and access them later.
@@ -18027,6 +17555,130 @@ const myWorkflow = createWorkflow(
```
+# Workflow Hooks
+
+In this chapter, you'll learn what a workflow hook is and how to consume them.
+
+## What is a Workflow Hook?
+
+A workflow hook is a point in a workflow where you can inject custom functionality as a step function, called a hook handler.
+
+Medusa exposes hooks in many of its workflows that are used in its API routes. You can consume those hooks to add your custom logic.
+
+Refer to the [Workflows Reference](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md) to view all workflows and their hooks.
+
+You want to perform a custom action during a workflow's execution, such as when a product is created.
+
+***
+
+## How to Consume a Hook?
+
+A workflow has a special `hooks` property which is an object that holds its hooks.
+
+So, in a TypeScript or JavaScript file created under the `src/workflows/hooks` directory:
+
+- Import the workflow.
+- Access its hook using the `hooks` property.
+- Pass the hook a step function as a parameter to consume it.
+
+For example, to consume the `productsCreated` hook of Medusa's `createProductsWorkflow`, create the file `src/workflows/hooks/product-created.ts` with the following content:
+
+```ts title="src/workflows/hooks/product-created.ts" highlights={handlerHighlights}
+import { createProductsWorkflow } from "@medusajs/medusa/core-flows"
+
+createProductsWorkflow.hooks.productsCreated(
+ async ({ products }, { container }) => {
+ // TODO perform an action
+ }
+)
+```
+
+The `productsCreated` hook is available on the workflow's `hooks` property by its name.
+
+You invoke the hook, passing a step function (the hook handler) as a parameter.
+
+Now, when a product is created using the [Create Product API route](https://docs.medusajs.com/api/admin#products_postproducts), your hook handler is executed after the product is created.
+
+A hook can have only one handler.
+
+Refer to the [createProductsWorkflow reference](https://docs.medusajs.com/resources/references/medusa-workflows/createProductsWorkflow/index.html.md) to see at which point the hook handler is executed.
+
+### Hook Handler Parameter
+
+Since a hook handler is essentially a step function, it receives the hook's input as a first parameter, and an object holding a `container` property as a second parameter.
+
+Each hook has different input. For example, the `productsCreated` hook receives an object having a `products` property holding the created product.
+
+### Hook Handler Compensation
+
+Since the hook handler is a step function, you can set its compensation function as a second parameter of the hook.
+
+For example:
+
+```ts title="src/workflows/hooks/product-created.ts"
+import { createProductsWorkflow } from "@medusajs/medusa/core-flows"
+
+createProductsWorkflow.hooks.productsCreated(
+ async ({ products }, { container }) => {
+ // TODO perform an action
+
+ return new StepResponse(undefined, { ids })
+ },
+ async ({ ids }, { container }) => {
+ // undo the performed action
+ }
+)
+```
+
+The compensation function is executed if an error occurs in the workflow to undo the actions performed by the hook handler.
+
+The compensation function receives as an input the second parameter passed to the `StepResponse` returned by the step function.
+
+It also accepts as a second parameter an object holding a `container` property to resolve resources from the Medusa container.
+
+### Additional Data Property
+
+Medusa's workflows pass in the hook's input an `additional_data` property:
+
+```ts title="src/workflows/hooks/product-created.ts" highlights={[["4", "additional_data"]]}
+import { createProductsWorkflow } from "@medusajs/medusa/core-flows"
+
+createProductsWorkflow.hooks.productsCreated(
+ async ({ products, additional_data }, { container }) => {
+ // TODO perform an action
+ }
+)
+```
+
+This property is an object that holds additional data passed to the workflow through the request sent to the API route using the workflow.
+
+Learn how to pass `additional_data` in requests to API routes in [this chapter](https://docs.medusajs.com/learn/fundamentals/api-routes/additional-data/index.html.md).
+
+### Pass Additional Data to Workflow
+
+You can also pass that additional data when executing the workflow. Pass it as a parameter to the `.run` method of the workflow:
+
+```ts title="src/workflows/hooks/product-created.ts" highlights={[["10", "additional_data"]]}
+import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
+import { createProductsWorkflow } from "@medusajs/medusa/core-flows"
+
+export async function POST(req: MedusaRequest, res: MedusaResponse) {
+ await createProductsWorkflow(req.scope).run({
+ input: {
+ products: [
+ // ...
+ ],
+ additional_data: {
+ custom_field: "test",
+ },
+ },
+ })
+}
+```
+
+Your hook handler then receives that passed data in the `additional_data` object.
+
+
# Workflow Timeout
In this chapter, you’ll learn how to set a timeout for workflows and steps.
@@ -18113,74 +17765,383 @@ This step's executions fail if they run longer than two seconds.
A step’s timeout error is returned in the `errors` property of the workflow’s execution, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/errors/index.html.md). The error’s name is `TransactionStepTimeoutError`.
-# Example: Integration Tests for a Module
+# Translate Medusa Admin
-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.
+The Medusa Admin supports multiple languages, with the default being English. In this documentation, you'll learn how to contribute to the community by translating the Medusa Admin to a language you're fluent in.
-### Prerequisites
+{/* vale docs.We = NO */}
-- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md)
+You can contribute either by translating the admin to a new language, or fixing translations for existing languages. As we can't validate every language's translations, some translations may be incorrect. Your contribution is welcome to fix any translation errors you find.
-## Write Integration Test for Module
+{/* vale docs.We = YES */}
-Consider a `blog` module with a `BlogModuleService` that has a `getMessage` method:
-
-```ts title="src/modules/blog/service.ts"
-import { MedusaService } from "@medusajs/framework/utils"
-import MyCustom from "./models/my-custom"
-
-class BlogModuleService extends MedusaService({
- MyCustom,
-}){
- getMessage(): string {
- return "Hello, World!"
- }
-}
-
-export default BlogModuleService
-```
-
-To create an integration test for the method, create the file `src/modules/blog/__tests__/service.spec.ts` with the following content:
-
-```ts title="src/modules/blog/__tests__/service.spec.ts"
-import { moduleIntegrationTestRunner } from "@medusajs/test-utils"
-import { BLOG_MODULE } from ".."
-import BlogModuleService from "../service"
-import MyCustom from "../models/my-custom"
-
-moduleIntegrationTestRunner({
- moduleName: BLOG_MODULE,
- moduleModels: [MyCustom],
- resolve: "./src/modules/blog",
- testSuite: ({ service }) => {
- describe("BlogModuleService", () => {
- it("says hello world", () => {
- const message = service.getMessage()
-
- expect(message).toEqual("Hello, World!")
- })
- })
- },
-})
-
-jest.setTimeout(60 * 1000)
-```
-
-You use the `moduleIntegrationTestRunner` function to add tests for the `blog` module. You have one test that passes if the `getMessage` method returns the `"Hello, World!"` string.
+Check out the translated languages either in the admin dashboard's settings or on [GitHub](https://github.com/medusajs/medusa/blob/develop/packages/admin/dashboard/src/i18n/languages.ts).
***
-## Run Test
+## How to Contribute Translation
-Run the following command to run your module integration tests:
+1. Clone the [Medusa monorepository](https://github.com/medusajs/medusa) to your local machine:
-```bash npm2yarn
-npm run test:integration:modules
+```bash
+git clone https://github.com/medusajs/medusa.git
```
-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).
+If you already have it cloned, make sure to pull the latest changes from the `develop` branch.
-This runs your Medusa application and runs the tests available in any `__tests__` directory under the `src/modules` directory.
+2. Install the monorepository's dependencies. Since it's a Yarn workspace, it's highly recommended to use yarn:
+
+```bash
+yarn install
+```
+
+3. Create a branch that you'll use to open the pull request later:
+
+```bash
+git checkout -b feat/translate-
+```
+
+Where `` is your language name. For example, `feat/translate-da`.
+
+4. Translation files are under `packages/admin/dashboard/src/i18n/translations` as JSON files whose names are the ISO-2 name of the language.
+ - If you're adding a new language, copy the file `packages/admin/dashboard/src/i18n/translations/en.json` and paste it with the ISO-2 name for your language. For example, if you're adding Danish translations, copy the `en.json` file and paste it as `packages/admin/dashboard/src/i18n/translations/de.json`.
+ - If you're fixing a translation, find the JSON file of the language under `packages/admin/dashboard/src/i18n/translations`.
+
+5. Start translating the keys in the JSON file (or updating the targeted ones). All keys in the JSON file must be translated, and your PR tests will fail otherwise.
+ - You can check whether the JSON file is valid by running the following command in `packages/admin/dashboard`, replacing `da.json` with the JSON file's name:
+
+```bash title="packages/admin/dashboard"
+yarn i18n:validate da.json
+```
+
+6. After finishing the translation, if you're adding a new language, import its JSON file in `packages/admin/dashboard/src/i18n/translations/index.ts` and add it to the exported object:
+
+```ts title="packages/admin/dashboard/src/i18n/translations/index.ts" highlights={[["2"], ["6"], ["7"], ["8"]]}
+// other imports...
+import da from "./da.json"
+
+export default {
+ // other languages...
+ da: {
+ translation: da,
+ },
+}
+```
+
+The language's key in the object is the ISO-2 name of the language.
+
+7. If you're adding a new language, add it to the file `packages/admin/dashboard/src/i18n/languages.ts`:
+
+```ts title="packages/admin/dashboard/src/i18n/languages.ts" highlights={languageHighlights}
+import { da } from "date-fns/locale"
+// other imports...
+
+export const languages: Language[] = [
+ // other languages...
+ {
+ code: "da",
+ display_name: "Danish",
+ ltr: true,
+ date_locale: da,
+ },
+]
+```
+
+`languages` is an array having the following properties:
+
+- `code`: The ISO-2 name of the language. For example, `da` for Danish.
+- `display_name`: The language's name to be displayed in the admin.
+- `ltr`: Whether the language supports a left-to-right layout. For example, set this to `false` for languages like Arabic.
+- `date_locale`: An instance of the locale imported from the [date-fns/locale](https://date-fns.org/) package.
+
+8. Once you're done, push the changes into your branch and open a pull request on GitHub.
+
+Our team will perform a general review on your PR and merge it if no issues are found. The translation will be available in the admin after the next release.
+
+
+# Docs Contribution Guidelines
+
+Thank you for your interest in contributing to the documentation! You will be helping the open source community and other developers interested in learning more about Medusa and using it.
+
+This guide is specific to contributing to the documentation. If you’re interested in contributing to Medusa’s codebase, check out the [contributing guidelines in the Medusa GitHub repository](https://github.com/medusajs/medusa/blob/develop/CONTRIBUTING.md).
+
+## What Can You Contribute?
+
+You can contribute to the Medusa documentation in the following ways:
+
+- Fixes to existing content. This includes small fixes like typos, or adding missing information.
+- Additions to the documentation. If you think a documentation page can be useful to other developers, you can contribute by adding it.
+ - Make sure to open an issue first in the [medusa repository](https://github.com/medusajs/medusa) to confirm that you can add that documentation page.
+- Fixes to UI components and tooling. If you find a bug while browsing the documentation, you can contribute by fixing it.
+
+***
+
+## Documentation Workspace
+
+Medusa's documentation projects are all part of the documentation `yarn` workspace, which you can find in the [medusa repository](https://github.com/medusajs/medusa) under the `www` directory.
+
+The workspace has the following two directories:
+
+- `apps`: this directory holds the different documentation websites and projects.
+ - `book`: includes the codebase for the [main Medusa documentation](https://docs.medusajs.com//index.html.md). It's built with [Next.js 15](https://nextjs.org/).
+ - `resources`: includes the codebase for the resources documentation, which powers different sections of the docs such as the [Integrations](https://docs.medusajs.com/resources/integrations/index.html.md) or [How-to & Tutorials](https://docs.medusajs.com/resources/how-to-tutorials/index.html.md) sections. It's built with [Next.js 15](https://nextjs.org/).
+ - `api-reference`: includes the codebase for the API reference website. It's built with [Next.js 15](https://nextjs.org/).
+ - `ui`: includes the codebase for the Medusa UI documentation website. It's built with [Next.js 15](https://nextjs.org/).
+- `packages`: this directory holds the shared packages and components necessary for the development of the projects in the `apps` directory.
+ - `docs-ui` includes the shared React components between the different apps.
+ - `remark-rehype-plugins` includes Remark and Rehype plugins used by the documentation projects.
+
+### Setup the Documentation Workspace
+
+### Prerequisites
+
+- [Node.js v20+](https://nodejs.org/en/download)
+- [Yarn v3+](https://v3.yarnpkg.com/getting-started/install)
+
+In the `www` directory, run the following command to install the dependencies:
+
+```bash
+yarn install
+```
+
+Then, run the following command to build packages under the `www/packages` directory:
+
+```bash
+yarn build
+```
+
+After that, you can change into the directory of any documentation project under the `www/apps` directory and run the `dev` command to start the development server.
+
+***
+
+## Documentation Content
+
+All documentation projects are built with Next.js. The content is writtin in MDX files.
+
+### Medusa Main Docs Content
+
+The content of the Medusa main docs are under the `www/apps/book/app` directory.
+
+### Medusa Resources Content
+
+The content of all pages under the `/resources` path are under the `www/apps/resources/app` directory.
+
+Documentation pages under the `www/apps/resources/references` directory are generated automatically from the source code under the `packages/medusa` directory. So, you can't directly make changes to them. Instead, you'll have to make changes to the comments in the original source code.
+
+### API Reference
+
+The API reference's content is split into two types:
+
+1. Static content, which are the content related to getting started, expanding fields, and more. These are located in the `www/apps/api-reference/markdown` directory. They are MDX files.
+2. OpenAPI specs that are shown to developers when checking the reference of an API Route. These are generated from OpenApi Spec comments, which are under the `www/utils/generated/oas-output` directory.
+
+### Medusa UI Documentation
+
+The content of the Medusa UI documentation are located under the `www/apps/ui/src/content/docs` directory. They are MDX files.
+
+The UI documentation also shows code examples, which are under the `www/apps/ui/src/examples` directory.
+
+The UI component props are generated from the source code and placed into the `www/apps/ui/src/specs` directory. To contribute to these props and their comments, check the comments in the source code under the `packages/design-system/ui` directory.
+
+***
+
+## Style Guide
+
+When you contribute to the documentation content, make sure to follow the [documentation style guide](https://www.notion.so/Style-Guide-Docs-fad86dd1c5f84b48b145e959f36628e0).
+
+***
+
+## How to Contribute
+
+If you’re fixing errors in an existing documentation page, you can scroll down to the end of the page and click on the “Edit this page” link. You’ll be redirected to the GitHub edit form of that page and you can make edits directly and submit a pull request (PR).
+
+If you’re adding a new page or contributing to the codebase, fork the repository, create a new branch, and make all changes necessary in your repository. Then, once you’re done, create a PR in the Medusa repository.
+
+### Base Branch
+
+When you make an edit to an existing documentation page or fork the repository to make changes to the documentation, create a new branch.
+
+Documentation contributions always use `develop` as the base branch. Make sure to also open your PR against the `develop` branch.
+
+### Branch Name
+
+Make sure that the branch name starts with `docs/`. For example, `docs/fix-services`. Vercel deployed previews are only triggered for branches starting with `docs/`.
+
+### Pull Request Conventions
+
+When you create a pull request, prefix the title with `docs:` or `docs(PROJECT_NAME):`, where `PROJECT_NAME` is the name of the documentation project this pull request pertains to. For example, `docs(ui): fix titles`.
+
+In the body of the PR, explain clearly what the PR does. If the PR solves an issue, use [closing keywords](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) with the issue number. For example, “Closes #1333”.
+
+***
+
+## Images
+
+If you are adding images to a documentation page, you can host the image on [Imgur](https://imgur.com) for free to include it in the PR. Our team will later upload it to our image hosting.
+
+***
+
+## NPM and Yarn Code Blocks
+
+If you’re adding code blocks that use NPM and Yarn, you must add the `npm2yarn` meta field.
+
+For example:
+
+````md
+```bash npm2yarn
+npm run start
+```
+````
+
+The code snippet must be written using NPM.
+
+### Global Option
+
+When a command uses the global option `-g`, add it at the end of the NPM command to ensure that it’s transformed to a Yarn command properly. For example:
+
+```bash npm2yarn
+npm install @medusajs/cli -g
+```
+
+***
+
+## Linting with Vale
+
+Medusa uses [Vale](https://vale.sh/) to lint documentation pages and perform checks on incoming PRs into the repository.
+
+### Result of Vale PR Checks
+
+You can check the result of running the "lint" action on your PR by clicking the Details link next to it. You can find there all errors that you need to fix.
+
+### Run Vale Locally
+
+If you want to check your work locally, you can do that by:
+
+1. [Installing Vale](https://vale.sh/docs/vale-cli/installation/) on your machine.
+2. Changing to the `www/vale` directory:
+
+```bash
+cd www/vale
+```
+
+3\. Running the `run-vale` script:
+
+```bash
+# to lint content for the main documentation
+./run-vale.sh book/app/learn error resources
+# to lint content for the resources documentation
+./run-vale.sh resources/app error
+# to lint content for the API reference
+./run-vale.sh api-reference/markdown error
+# to lint content for the Medusa UI documentation
+./run-vale.sh ui/src/content/docs error
+# to lint content for the user guide
+./run-vale.sh user-guide/app error
+```
+
+{/* TODO need to enable MDX v1 comments first. */}
+
+{/* ### Linter Exceptions
+
+If it's needed to break some style guide rules in a document, you can wrap the parts that the linter shouldn't scan with the following comments in the `md` or `mdx` files:
+
+```md
+
+
+content that shouldn't be scanned for errors here...
+
+
+```
+
+You can also disable specific rules. For example:
+
+```md
+
+
+Medusa supports Node versions 14 and 16.
+
+
+```
+
+If you use this in your PR, you must justify its usage. */}
+
+***
+
+## Linting with ESLint
+
+Medusa uses ESlint to lint code blocks both in the content and the code base of the documentation apps.
+
+### Linting Content with ESLint
+
+Each PR runs through a check that lints the code in the content files using ESLint. The action's name is `content-eslint`.
+
+If you want to check content ESLint errors locally and fix them, you can do that by:
+
+1\. Install the dependencies in the `www` directory:
+
+```bash
+yarn install
+```
+
+2\. Run the turbo command in the `www` directory:
+
+```bash
+turbo run lint:content
+```
+
+This will fix any fixable errors, and show errors that require your action.
+
+### Linting Code with ESLint
+
+Each PR runs through a check that lints the code in the content files using ESLint. The action's name is `code-docs-eslint`.
+
+If you want to check code ESLint errors locally and fix them, you can do that by:
+
+1\. Install the dependencies in the `www` directory:
+
+```bash
+yarn install
+```
+
+2\. Run the turbo command in the `www` directory:
+
+```bash
+yarn lint
+```
+
+This will fix any fixable errors, and show errors that require your action.
+
+{/* TODO need to enable MDX v1 comments first. */}
+
+{/* ### ESLint Exceptions
+
+If some code blocks have errors that can't or shouldn't be fixed, you can add the following command before the code block:
+
+~~~md
+
+
+```js
+console.log("This block isn't linted")
+```
+
+```js
+console.log("This block is linted")
+```
+~~~
+
+You can also disable specific rules. For example:
+
+~~~md
+
+
+```js
+console.log("This block can use semicolons");
+```
+
+```js
+console.log("This block can't use semi colons")
+```
+~~~ */}
# Example: Write Integration Tests for API Routes
@@ -18883,6 +18844,76 @@ The `errors` property contains an array of errors thrown during the execution of
If you threw a `MedusaError`, then you can check the error message in `errors[0].error.message`.
+# Example: Integration Tests for a Module
+
+In this chapter, find an example of writing an integration test for a module using [moduleIntegrationTestRunner](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/modules-tests/index.html.md) from Medusa's Testing Framework.
+
+### Prerequisites
+
+- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md)
+
+## Write Integration Test for Module
+
+Consider a `blog` module with a `BlogModuleService` that has a `getMessage` method:
+
+```ts title="src/modules/blog/service.ts"
+import { MedusaService } from "@medusajs/framework/utils"
+import MyCustom from "./models/my-custom"
+
+class BlogModuleService extends MedusaService({
+ MyCustom,
+}){
+ getMessage(): string {
+ return "Hello, World!"
+ }
+}
+
+export default BlogModuleService
+```
+
+To create an integration test for the method, create the file `src/modules/blog/__tests__/service.spec.ts` with the following content:
+
+```ts title="src/modules/blog/__tests__/service.spec.ts"
+import { moduleIntegrationTestRunner } from "@medusajs/test-utils"
+import { BLOG_MODULE } from ".."
+import BlogModuleService from "../service"
+import MyCustom from "../models/my-custom"
+
+moduleIntegrationTestRunner({
+ moduleName: BLOG_MODULE,
+ moduleModels: [MyCustom],
+ resolve: "./src/modules/blog",
+ testSuite: ({ service }) => {
+ describe("BlogModuleService", () => {
+ it("says hello world", () => {
+ const message = service.getMessage()
+
+ expect(message).toEqual("Hello, World!")
+ })
+ })
+ },
+})
+
+jest.setTimeout(60 * 1000)
+```
+
+You use the `moduleIntegrationTestRunner` function to add tests for the `blog` module. You have one test that passes if the `getMessage` method returns the `"Hello, World!"` string.
+
+***
+
+## Run Test
+
+Run the following command to run your module integration tests:
+
+```bash npm2yarn
+npm run test:integration:modules
+```
+
+If you don't have a `test:integration:modules` script in `package.json`, refer to the [Medusa Testing Tools chapter](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools#add-test-commands/index.html.md).
+
+This runs your Medusa application and runs the tests available in any `__tests__` directory under the `src/modules` directory.
+
+
# Commerce Modules
In this section of the documentation, you'll find guides and references related to Medusa's Commerce Modules.
@@ -19063,154 +19094,6 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc
***
-# Currency Module
-
-In this section of the documentation, you will find resources to learn more about the Currency Module and how to use it in your application.
-
-Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/store/index.html.md) to learn how to manage your store's currencies using the dashboard.
-
-Medusa has currency related features available out-of-the-box through the Currency Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Currency Module.
-
-Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).
-
-## Currency Features
-
-- [Currency Management and Retrieval](https://docs.medusajs.com/references/currency/listAndCountCurrencies/index.html.md): This module adds all common currencies to your application and allows you to retrieve them.
-- [Support Currencies in Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/currency/links-to-other-modules/index.html.md): Other Commerce Modules use currency codes in their data models or operations. Use the Currency Module to retrieve a currency code and its details.
-
-***
-
-## How to Use the Currency Module
-
-In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.
-
-You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.
-
-For example:
-
-```ts title="src/workflows/retrieve-price-with-currency.ts" highlights={highlights}
-import {
- createWorkflow,
- WorkflowResponse,
- createStep,
- StepResponse,
- transform,
-} from "@medusajs/framework/workflows-sdk"
-import { Modules } from "@medusajs/framework/utils"
-
-const retrieveCurrencyStep = createStep(
- "retrieve-currency",
- async ({}, { container }) => {
- const currencyModuleService = container.resolve(Modules.CURRENCY)
-
- const currency = await currencyModuleService
- .retrieveCurrency("usd")
-
- return new StepResponse({ currency })
- }
-)
-
-type Input = {
- price: number
-}
-
-export const retrievePriceWithCurrency = createWorkflow(
- "create-currency",
- (input: Input) => {
- const { currency } = retrieveCurrencyStep()
-
- const formattedPrice = transform({
- input,
- currency,
- }, (data) => {
- return `${data.currency.symbol}${data.input.price}`
- })
-
- return new WorkflowResponse({
- formattedPrice,
- })
- }
-)
-```
-
-You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:
-
-### API Route
-
-```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"], ["13"], ["14"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
-import type {
- MedusaRequest,
- MedusaResponse,
-} from "@medusajs/framework/http"
-import { retrievePriceWithCurrency } from "../../workflows/retrieve-price-with-currency"
-
-export async function GET(
- req: MedusaRequest,
- res: MedusaResponse
-) {
- const { result } = await retrievePriceWithCurrency(req.scope)
- .run({
- price: 10,
- })
-
- res.send(result)
-}
-```
-
-### Subscriber
-
-```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"], ["13"], ["14"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
-import {
- type SubscriberConfig,
- type SubscriberArgs,
-} from "@medusajs/framework"
-import { retrievePriceWithCurrency } from "../workflows/retrieve-price-with-currency"
-
-export default async function handleUserCreated({
- event: { data },
- container,
-}: SubscriberArgs<{ id: string }>) {
- const { result } = await retrievePriceWithCurrency(container)
- .run({
- price: 10,
- })
-
- console.log(result)
-}
-
-export const config: SubscriberConfig = {
- event: "user.created",
-}
-```
-
-### Scheduled Job
-
-```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"], ["9"], ["10"]]}
-import { MedusaContainer } from "@medusajs/framework/types"
-import { retrievePriceWithCurrency } from "../workflows/retrieve-price-with-currency"
-
-export default async function myCustomJob(
- container: MedusaContainer
-) {
- const { result } = await retrievePriceWithCurrency(container)
- .run({
- price: 10,
- })
-
- console.log(result)
-}
-
-export const config = {
- name: "run-once-a-day",
- schedule: `0 0 * * *`,
-}
-```
-
-Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).
-
-***
-
-
# Auth Module
In this section of the documentation, you will find resources to learn more about the Auth Module and how to use it in your application.
@@ -19341,24 +19224,24 @@ Medusa provides the following authentication providers out-of-the-box. You can u
***
-# Cart Module
+# Customer 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.
+In this section of the documentation, you will find resources to learn more about the Customer Module and how to use it in your application.
-Medusa has cart related features available out-of-the-box through the Cart Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Cart Module.
+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).
-## Cart Features
+## Customer Features
-- [Cart Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/concepts/index.html.md): Store and manage carts, including their addresses, line items, shipping methods, and more.
-- [Apply Promotion Adjustments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/promotions/index.html.md): Apply promotions or discounts to line items and shipping methods by adding adjustment lines that are factored into their subtotals.
-- [Apply Tax Lines](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/tax-lines/index.html.md): Apply tax lines to line items and shipping methods.
-- [Cart Scoping](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/links-to-other-modules/index.html.md): When used in the Medusa application, Medusa creates links to other Commerce Modules, scoping a cart to a sales channel, region, and a customer.
+- [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 Cart Module
+## 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.
@@ -19366,7 +19249,7 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows
For example:
-```ts title="src/workflows/create-cart.ts" highlights={highlights}
+```ts title="src/workflows/create-customer.ts" highlights={highlights}
import {
createWorkflow,
WorkflowResponse,
@@ -19375,45 +19258,36 @@ import {
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"
-const createCartStep = createStep(
- "create-cart",
+const createCustomerStep = createStep(
+ "create-customer",
async ({}, { container }) => {
- const cartModuleService = container.resolve(Modules.CART)
+ const customerModuleService = container.resolve(Modules.CUSTOMER)
- const cart = await cartModuleService.createCarts({
- currency_code: "usd",
- shipping_address: {
- address_1: "1512 Barataria Blvd",
- country_code: "us",
- },
- items: [
- {
- title: "Shirt",
- unit_price: 1000,
- quantity: 1,
- },
- ],
+ const customer = await customerModuleService.createCustomers({
+ first_name: "Peter",
+ last_name: "Hayes",
+ email: "peter.hayes@example.com",
})
- return new StepResponse({ cart }, cart.id)
+ return new StepResponse({ customer }, customer.id)
},
- async (cartId, { container }) => {
- if (!cartId) {
+ async (customerId, { container }) => {
+ if (!customerId) {
return
}
- const cartModuleService = container.resolve(Modules.CART)
+ const customerModuleService = container.resolve(Modules.CUSTOMER)
- await cartModuleService.deleteCarts([cartId])
+ await customerModuleService.deleteCustomers([customerId])
}
)
-export const createCartWorkflow = createWorkflow(
- "create-cart",
+export const createCustomerWorkflow = createWorkflow(
+ "create-customer",
() => {
- const { cart } = createCartStep()
+ const { customer } = createCustomerStep()
return new WorkflowResponse({
- cart,
+ customer,
})
}
)
@@ -19428,13 +19302,13 @@ import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
-import { createCartWorkflow } from "../../workflows/create-cart"
+import { createCustomerWorkflow } from "../../workflows/create-customer"
export async function GET(
req: MedusaRequest,
res: MedusaResponse
) {
- const { result } = await createCartWorkflow(req.scope)
+ const { result } = await createCustomerWorkflow(req.scope)
.run()
res.send(result)
@@ -19448,13 +19322,13 @@ import {
type SubscriberConfig,
type SubscriberArgs,
} from "@medusajs/framework"
-import { createCartWorkflow } from "../workflows/create-cart"
+import { createCustomerWorkflow } from "../workflows/create-customer"
export default async function handleUserCreated({
event: { data },
container,
}: SubscriberArgs<{ id: string }>) {
- const { result } = await createCartWorkflow(container)
+ const { result } = await createCustomerWorkflow(container)
.run()
console.log(result)
@@ -19469,12 +19343,12 @@ export const config: SubscriberConfig = {
```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
import { MedusaContainer } from "@medusajs/framework/types"
-import { createCartWorkflow } from "../workflows/create-cart"
+import { createCustomerWorkflow } from "../workflows/create-customer"
export default async function myCustomJob(
container: MedusaContainer
) {
- const { result } = await createCartWorkflow(container)
+ const { result } = await createCustomerWorkflow(container)
.run()
console.log(result)
@@ -19658,24 +19532,27 @@ The Fulfillment Module accepts options for further configurations. Refer to [thi
***
-# Customer Module
+# Inventory 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 Inventory 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](https://docs.medusajs.com/user-guide/inventory/index.html.md) to learn how to manage inventory and related features 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.
+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).
-## Customer Features
+## Inventory 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).
+- [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 Customer Module
+## 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.
@@ -19683,7 +19560,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-inventory-item.ts" highlights={highlights}
import {
createWorkflow,
WorkflowResponse,
@@ -19692,36 +19569,36 @@ import {
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"
-const createCustomerStep = createStep(
- "create-customer",
+const createInventoryItemStep = createStep(
+ "create-inventory-item",
async ({}, { container }) => {
- const customerModuleService = container.resolve(Modules.CUSTOMER)
+ const inventoryModuleService = container.resolve(Modules.INVENTORY)
- const customer = await customerModuleService.createCustomers({
- first_name: "Peter",
- last_name: "Hayes",
- email: "peter.hayes@example.com",
+ const inventoryItem = await inventoryModuleService.createInventoryItems({
+ sku: "SHIRT",
+ title: "Green Medusa Shirt",
+ requires_shipping: true,
})
- return new StepResponse({ customer }, customer.id)
+ return new StepResponse({ inventoryItem }, inventoryItem.id)
},
- async (customerId, { container }) => {
- if (!customerId) {
+ async (inventoryItemId, { container }) => {
+ if (!inventoryItemId) {
return
}
- const customerModuleService = container.resolve(Modules.CUSTOMER)
+ const inventoryModuleService = container.resolve(Modules.INVENTORY)
- await customerModuleService.deleteCustomers([customerId])
+ await inventoryModuleService.deleteInventoryItems([inventoryItemId])
}
)
-export const createCustomerWorkflow = createWorkflow(
- "create-customer",
+export const createInventoryItemWorkflow = createWorkflow(
+ "create-inventory-item-workflow",
() => {
- const { customer } = createCustomerStep()
+ const { inventoryItem } = createInventoryItemStep()
return new WorkflowResponse({
- customer,
+ inventoryItem,
})
}
)
@@ -19736,13 +19613,13 @@ import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
-import { createCustomerWorkflow } from "../../workflows/create-customer"
+import { createInventoryItemWorkflow } from "../../workflows/create-inventory-item"
export async function GET(
req: MedusaRequest,
res: MedusaResponse
) {
- const { result } = await createCustomerWorkflow(req.scope)
+ const { result } = await createInventoryItemWorkflow(req.scope)
.run()
res.send(result)
@@ -19756,13 +19633,13 @@ import {
type SubscriberConfig,
type SubscriberArgs,
} from "@medusajs/framework"
-import { createCustomerWorkflow } from "../workflows/create-customer"
+import { createInventoryItemWorkflow } from "../workflows/create-inventory-item"
export default async function handleUserCreated({
event: { data },
container,
}: SubscriberArgs<{ id: string }>) {
- const { result } = await createCustomerWorkflow(container)
+ const { result } = await createInventoryItemWorkflow(container)
.run()
console.log(result)
@@ -19777,12 +19654,310 @@ 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 { createInventoryItemWorkflow } from "../workflows/create-inventory-item"
export default async function myCustomJob(
container: MedusaContainer
) {
- const { result } = await createCustomerWorkflow(container)
+ 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).
+
+***
+
+
+# Currency Module
+
+In this section of the documentation, you will find resources to learn more about the Currency Module and how to use it in your application.
+
+Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/store/index.html.md) to learn how to manage your store's currencies using the dashboard.
+
+Medusa has currency related features available out-of-the-box through the Currency Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Currency Module.
+
+Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).
+
+## Currency Features
+
+- [Currency Management and Retrieval](https://docs.medusajs.com/references/currency/listAndCountCurrencies/index.html.md): This module adds all common currencies to your application and allows you to retrieve them.
+- [Support Currencies in Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/currency/links-to-other-modules/index.html.md): Other Commerce Modules use currency codes in their data models or operations. Use the Currency Module to retrieve a currency code and its details.
+
+***
+
+## How to Use the Currency Module
+
+In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.
+
+You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.
+
+For example:
+
+```ts title="src/workflows/retrieve-price-with-currency.ts" highlights={highlights}
+import {
+ createWorkflow,
+ WorkflowResponse,
+ createStep,
+ StepResponse,
+ transform,
+} from "@medusajs/framework/workflows-sdk"
+import { Modules } from "@medusajs/framework/utils"
+
+const retrieveCurrencyStep = createStep(
+ "retrieve-currency",
+ async ({}, { container }) => {
+ const currencyModuleService = container.resolve(Modules.CURRENCY)
+
+ const currency = await currencyModuleService
+ .retrieveCurrency("usd")
+
+ return new StepResponse({ currency })
+ }
+)
+
+type Input = {
+ price: number
+}
+
+export const retrievePriceWithCurrency = createWorkflow(
+ "create-currency",
+ (input: Input) => {
+ const { currency } = retrieveCurrencyStep()
+
+ const formattedPrice = transform({
+ input,
+ currency,
+ }, (data) => {
+ return `${data.currency.symbol}${data.input.price}`
+ })
+
+ return new WorkflowResponse({
+ formattedPrice,
+ })
+ }
+)
+```
+
+You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:
+
+### API Route
+
+```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"], ["13"], ["14"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
+import type {
+ MedusaRequest,
+ MedusaResponse,
+} from "@medusajs/framework/http"
+import { retrievePriceWithCurrency } from "../../workflows/retrieve-price-with-currency"
+
+export async function GET(
+ req: MedusaRequest,
+ res: MedusaResponse
+) {
+ const { result } = await retrievePriceWithCurrency(req.scope)
+ .run({
+ price: 10,
+ })
+
+ res.send(result)
+}
+```
+
+### Subscriber
+
+```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"], ["13"], ["14"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
+import {
+ type SubscriberConfig,
+ type SubscriberArgs,
+} from "@medusajs/framework"
+import { retrievePriceWithCurrency } from "../workflows/retrieve-price-with-currency"
+
+export default async function handleUserCreated({
+ event: { data },
+ container,
+}: SubscriberArgs<{ id: string }>) {
+ const { result } = await retrievePriceWithCurrency(container)
+ .run({
+ price: 10,
+ })
+
+ console.log(result)
+}
+
+export const config: SubscriberConfig = {
+ event: "user.created",
+}
+```
+
+### Scheduled Job
+
+```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"], ["9"], ["10"]]}
+import { MedusaContainer } from "@medusajs/framework/types"
+import { retrievePriceWithCurrency } from "../workflows/retrieve-price-with-currency"
+
+export default async function myCustomJob(
+ container: MedusaContainer
+) {
+ const { result } = await retrievePriceWithCurrency(container)
+ .run({
+ price: 10,
+ })
+
+ console.log(result)
+}
+
+export const config = {
+ name: "run-once-a-day",
+ schedule: `0 0 * * *`,
+}
+```
+
+Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).
+
+***
+
+
+# Cart Module
+
+In this section of the documentation, you will find resources to learn more about the Cart Module and how to use it in your application.
+
+Medusa has cart related features available out-of-the-box through the Cart Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Cart Module.
+
+Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).
+
+## Cart Features
+
+- [Cart Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/concepts/index.html.md): Store and manage carts, including their addresses, line items, shipping methods, and more.
+- [Apply Promotion Adjustments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/promotions/index.html.md): Apply promotions or discounts to line items and shipping methods by adding adjustment lines that are factored into their subtotals.
+- [Apply Tax Lines](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/tax-lines/index.html.md): Apply tax lines to line items and shipping methods.
+- [Cart Scoping](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/links-to-other-modules/index.html.md): When used in the Medusa application, Medusa creates links to other Commerce Modules, scoping a cart to a sales channel, region, and a customer.
+
+***
+
+## How to Use the Cart Module
+
+In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.
+
+You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.
+
+For example:
+
+```ts title="src/workflows/create-cart.ts" highlights={highlights}
+import {
+ createWorkflow,
+ WorkflowResponse,
+ createStep,
+ StepResponse,
+} from "@medusajs/framework/workflows-sdk"
+import { Modules } from "@medusajs/framework/utils"
+
+const createCartStep = createStep(
+ "create-cart",
+ async ({}, { container }) => {
+ const cartModuleService = container.resolve(Modules.CART)
+
+ const cart = await cartModuleService.createCarts({
+ currency_code: "usd",
+ shipping_address: {
+ address_1: "1512 Barataria Blvd",
+ country_code: "us",
+ },
+ items: [
+ {
+ title: "Shirt",
+ unit_price: 1000,
+ quantity: 1,
+ },
+ ],
+ })
+
+ return new StepResponse({ cart }, cart.id)
+ },
+ async (cartId, { container }) => {
+ if (!cartId) {
+ return
+ }
+ const cartModuleService = container.resolve(Modules.CART)
+
+ await cartModuleService.deleteCarts([cartId])
+ }
+)
+
+export const createCartWorkflow = createWorkflow(
+ "create-cart",
+ () => {
+ const { cart } = createCartStep()
+
+ return new WorkflowResponse({
+ cart,
+ })
+ }
+)
+```
+
+You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:
+
+### API Route
+
+```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
+import type {
+ MedusaRequest,
+ MedusaResponse,
+} from "@medusajs/framework/http"
+import { createCartWorkflow } from "../../workflows/create-cart"
+
+export async function GET(
+ req: MedusaRequest,
+ res: MedusaResponse
+) {
+ const { result } = await createCartWorkflow(req.scope)
+ .run()
+
+ res.send(result)
+}
+```
+
+### Subscriber
+
+```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
+import {
+ type SubscriberConfig,
+ type SubscriberArgs,
+} from "@medusajs/framework"
+import { createCartWorkflow } from "../workflows/create-cart"
+
+export default async function handleUserCreated({
+ event: { data },
+ container,
+}: SubscriberArgs<{ id: string }>) {
+ const { result } = await createCartWorkflow(container)
+ .run()
+
+ console.log(result)
+}
+
+export const config: SubscriberConfig = {
+ event: "user.created",
+}
+```
+
+### Scheduled Job
+
+```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
+import { MedusaContainer } from "@medusajs/framework/types"
+import { createCartWorkflow } from "../workflows/create-cart"
+
+export default async function myCustomJob(
+ container: MedusaContainer
+) {
+ const { result } = await createCartWorkflow(container)
.run()
console.log(result)
@@ -19955,150 +20130,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).
-
-***
-
-
# Payment Module
In this section of the documentation, you will find resources to learn more about the Payment Module and how to use it in your application.
@@ -20563,154 +20594,6 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc
***
-# Promotion Module
-
-In this section of the documentation, you will find resources to learn more about the Promotion Module and how to use it in your application.
-
-Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/promotions/index.html.md) to learn how to manage promotions using the dashboard.
-
-Medusa has promotion related features available out-of-the-box through the Promotion Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Promotion Module.
-
-Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).
-
-## Promotion Features
-
-- [Discount Functionalities](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/concepts/index.html.md): A promotion discounts an amount or percentage of a cart's items, shipping methods, or the entire order.
-- [Flexible Promotion Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/concepts#flexible-rules/index.html.md): A promotion has rules that restricts when the promotion is applied.
-- [Campaign Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/campaign/index.html.md): A campaign combines promotions under the same conditions, such as start and end dates, and budget configurations.
-- [Apply Promotion on Carts and Orders](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/actions/index.html.md): Apply promotions on carts and orders to discount items, shipping methods, or the entire order.
-
-***
-
-## How to Use the Promotion Module
-
-In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.
-
-You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.
-
-For example:
-
-```ts title="src/workflows/create-promotion.ts" highlights={highlights}
-import {
- createWorkflow,
- WorkflowResponse,
- createStep,
- StepResponse,
-} from "@medusajs/framework/workflows-sdk"
-import { Modules } from "@medusajs/framework/utils"
-
-const createPromotionStep = createStep(
- "create-promotion",
- async ({}, { container }) => {
- const promotionModuleService = container.resolve(Modules.PROMOTION)
-
- const promotion = await promotionModuleService.createPromotions({
- code: "10%OFF",
- type: "standard",
- application_method: {
- type: "percentage",
- target_type: "order",
- value: 10,
- currency_code: "usd",
- },
- })
-
- return new StepResponse({ promotion }, promotion.id)
- },
- async (promotionId, { container }) => {
- if (!promotionId) {
- return
- }
- const promotionModuleService = container.resolve(Modules.PROMOTION)
-
- await promotionModuleService.deletePromotions(promotionId)
- }
-)
-
-export const createPromotionWorkflow = createWorkflow(
- "create-promotion",
- () => {
- const { promotion } = createPromotionStep()
-
- return new WorkflowResponse({
- promotion,
- })
- }
-)
-```
-
-You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:
-
-### API Route
-
-```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
-import type {
- MedusaRequest,
- MedusaResponse,
-} from "@medusajs/framework/http"
-import { createPromotionWorkflow } from "../../workflows/create-cart"
-
-export async function GET(
- req: MedusaRequest,
- res: MedusaResponse
-) {
- const { result } = await createPromotionWorkflow(req.scope)
- .run()
-
- res.send(result)
-}
-```
-
-### Subscriber
-
-```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
-import {
- type SubscriberConfig,
- type SubscriberArgs,
-} from "@medusajs/framework"
-import { createPromotionWorkflow } from "../workflows/create-cart"
-
-export default async function handleUserCreated({
- event: { data },
- container,
-}: SubscriberArgs<{ id: string }>) {
- const { result } = await createPromotionWorkflow(container)
- .run()
-
- console.log(result)
-}
-
-export const config: SubscriberConfig = {
- event: "user.created",
-}
-```
-
-### Scheduled Job
-
-```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
-import { MedusaContainer } from "@medusajs/framework/types"
-import { createPromotionWorkflow } from "../workflows/create-cart"
-
-export default async function myCustomJob(
- container: MedusaContainer
-) {
- const { result } = await createPromotionWorkflow(container)
- .run()
-
- console.log(result)
-}
-
-export const config = {
- name: "run-once-a-day",
- schedule: `0 0 * * *`,
-}
-```
-
-Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).
-
-***
-
-
# 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.
@@ -21014,6 +20897,154 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc
***
+# Promotion Module
+
+In this section of the documentation, you will find resources to learn more about the Promotion Module and how to use it in your application.
+
+Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/promotions/index.html.md) to learn how to manage promotions using the dashboard.
+
+Medusa has promotion related features available out-of-the-box through the Promotion Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Promotion Module.
+
+Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).
+
+## Promotion Features
+
+- [Discount Functionalities](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/concepts/index.html.md): A promotion discounts an amount or percentage of a cart's items, shipping methods, or the entire order.
+- [Flexible Promotion Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/concepts#flexible-rules/index.html.md): A promotion has rules that restricts when the promotion is applied.
+- [Campaign Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/campaign/index.html.md): A campaign combines promotions under the same conditions, such as start and end dates, and budget configurations.
+- [Apply Promotion on Carts and Orders](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/actions/index.html.md): Apply promotions on carts and orders to discount items, shipping methods, or the entire order.
+
+***
+
+## How to Use the Promotion Module
+
+In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.
+
+You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.
+
+For example:
+
+```ts title="src/workflows/create-promotion.ts" highlights={highlights}
+import {
+ createWorkflow,
+ WorkflowResponse,
+ createStep,
+ StepResponse,
+} from "@medusajs/framework/workflows-sdk"
+import { Modules } from "@medusajs/framework/utils"
+
+const createPromotionStep = createStep(
+ "create-promotion",
+ async ({}, { container }) => {
+ const promotionModuleService = container.resolve(Modules.PROMOTION)
+
+ const promotion = await promotionModuleService.createPromotions({
+ code: "10%OFF",
+ type: "standard",
+ application_method: {
+ type: "percentage",
+ target_type: "order",
+ value: 10,
+ currency_code: "usd",
+ },
+ })
+
+ return new StepResponse({ promotion }, promotion.id)
+ },
+ async (promotionId, { container }) => {
+ if (!promotionId) {
+ return
+ }
+ const promotionModuleService = container.resolve(Modules.PROMOTION)
+
+ await promotionModuleService.deletePromotions(promotionId)
+ }
+)
+
+export const createPromotionWorkflow = createWorkflow(
+ "create-promotion",
+ () => {
+ const { promotion } = createPromotionStep()
+
+ return new WorkflowResponse({
+ promotion,
+ })
+ }
+)
+```
+
+You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:
+
+### API Route
+
+```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
+import type {
+ MedusaRequest,
+ MedusaResponse,
+} from "@medusajs/framework/http"
+import { createPromotionWorkflow } from "../../workflows/create-cart"
+
+export async function GET(
+ req: MedusaRequest,
+ res: MedusaResponse
+) {
+ const { result } = await createPromotionWorkflow(req.scope)
+ .run()
+
+ res.send(result)
+}
+```
+
+### Subscriber
+
+```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
+import {
+ type SubscriberConfig,
+ type SubscriberArgs,
+} from "@medusajs/framework"
+import { createPromotionWorkflow } from "../workflows/create-cart"
+
+export default async function handleUserCreated({
+ event: { data },
+ container,
+}: SubscriberArgs<{ id: string }>) {
+ const { result } = await createPromotionWorkflow(container)
+ .run()
+
+ console.log(result)
+}
+
+export const config: SubscriberConfig = {
+ event: "user.created",
+}
+```
+
+### Scheduled Job
+
+```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
+import { MedusaContainer } from "@medusajs/framework/types"
+import { createPromotionWorkflow } from "../workflows/create-cart"
+
+export default async function myCustomJob(
+ container: MedusaContainer
+) {
+ const { result } = await createPromotionWorkflow(container)
+ .run()
+
+ console.log(result)
+}
+
+export const config = {
+ name: "run-once-a-day",
+ schedule: `0 0 * * *`,
+}
+```
+
+Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).
+
+***
+
+
# 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.
@@ -21151,147 +21182,6 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc
***
-# Store Module
-
-In this section of the documentation, you will find resources to learn more about the Store Module and how to use it in your application.
-
-Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/store/index.html.md) to learn how to manage your store using the dashboard.
-
-Medusa has store related features available out-of-the-box through the Store Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Store Module.
-
-Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).
-
-## Store Features
-
-- [Store Management](https://docs.medusajs.com/references/store/models/Store/index.html.md): Create and manage stores in your application.
-- [Multi-Tenancy Support](https://docs.medusajs.com/references/store/models/Store/index.html.md): Create multiple stores, each having its own configurations.
-
-***
-
-## How to Use Store 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-store.ts" highlights={highlights}
-import {
- createWorkflow,
- WorkflowResponse,
- createStep,
- StepResponse,
-} from "@medusajs/framework/workflows-sdk"
-import { Modules } from "@medusajs/framework/utils"
-
-const createStoreStep = createStep(
- "create-store",
- async ({}, { container }) => {
- const storeModuleService = container.resolve(Modules.STORE)
-
- const store = await storeModuleService.createStores({
- name: "My Store",
- supported_currencies: [{
- currency_code: "usd",
- is_default: true,
- }],
- })
-
- return new StepResponse({ store }, store.id)
- },
- async (storeId, { container }) => {
- if(!storeId) {
- return
- }
- const storeModuleService = container.resolve(Modules.STORE)
-
- await storeModuleService.deleteStores([storeId])
- }
-)
-
-export const createStoreWorkflow = createWorkflow(
- "create-store",
- () => {
- const { store } = createStoreStep()
-
- return new WorkflowResponse({ store })
- }
-)
-```
-
-You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:
-
-### API Route
-
-```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
-import type {
- MedusaRequest,
- MedusaResponse,
-} from "@medusajs/framework/http"
-import { createStoreWorkflow } from "../../workflows/create-store"
-
-export async function GET(
- req: MedusaRequest,
- res: MedusaResponse
-) {
- const { result } = await createStoreWorkflow(req.scope)
- .run()
-
- res.send(result)
-}
-```
-
-### Subscriber
-
-```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
-import {
- type SubscriberConfig,
- type SubscriberArgs,
-} from "@medusajs/framework"
-import { createStoreWorkflow } from "../workflows/create-store"
-
-export default async function handleUserCreated({
- event: { data },
- container,
-}: SubscriberArgs<{ id: string }>) {
- const { result } = await createStoreWorkflow(container)
- .run()
-
- console.log(result)
-}
-
-export const config: SubscriberConfig = {
- event: "user.created",
-}
-```
-
-### Scheduled Job
-
-```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
-import { MedusaContainer } from "@medusajs/framework/types"
-import { createStoreWorkflow } from "../workflows/create-store"
-
-export default async function myCustomJob(
- container: MedusaContainer
-) {
- const { result } = await createStoreWorkflow(container)
- .run()
-
- console.log(result)
-}
-
-export const config = {
- name: "run-once-a-day",
- schedule: `0 0 * * *`,
-}
-```
-
-Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).
-
-***
-
-
# 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.
@@ -21437,6 +21327,147 @@ The Tax Module accepts options for further configurations. Refer to [this docume
***
+# Store Module
+
+In this section of the documentation, you will find resources to learn more about the Store Module and how to use it in your application.
+
+Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/store/index.html.md) to learn how to manage your store using the dashboard.
+
+Medusa has store related features available out-of-the-box through the Store Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Store Module.
+
+Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).
+
+## Store Features
+
+- [Store Management](https://docs.medusajs.com/references/store/models/Store/index.html.md): Create and manage stores in your application.
+- [Multi-Tenancy Support](https://docs.medusajs.com/references/store/models/Store/index.html.md): Create multiple stores, each having its own configurations.
+
+***
+
+## How to Use Store 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-store.ts" highlights={highlights}
+import {
+ createWorkflow,
+ WorkflowResponse,
+ createStep,
+ StepResponse,
+} from "@medusajs/framework/workflows-sdk"
+import { Modules } from "@medusajs/framework/utils"
+
+const createStoreStep = createStep(
+ "create-store",
+ async ({}, { container }) => {
+ const storeModuleService = container.resolve(Modules.STORE)
+
+ const store = await storeModuleService.createStores({
+ name: "My Store",
+ supported_currencies: [{
+ currency_code: "usd",
+ is_default: true,
+ }],
+ })
+
+ return new StepResponse({ store }, store.id)
+ },
+ async (storeId, { container }) => {
+ if(!storeId) {
+ return
+ }
+ const storeModuleService = container.resolve(Modules.STORE)
+
+ await storeModuleService.deleteStores([storeId])
+ }
+)
+
+export const createStoreWorkflow = createWorkflow(
+ "create-store",
+ () => {
+ const { store } = createStoreStep()
+
+ return new WorkflowResponse({ store })
+ }
+)
+```
+
+You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:
+
+### API Route
+
+```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
+import type {
+ MedusaRequest,
+ MedusaResponse,
+} from "@medusajs/framework/http"
+import { createStoreWorkflow } from "../../workflows/create-store"
+
+export async function GET(
+ req: MedusaRequest,
+ res: MedusaResponse
+) {
+ const { result } = await createStoreWorkflow(req.scope)
+ .run()
+
+ res.send(result)
+}
+```
+
+### Subscriber
+
+```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
+import {
+ type SubscriberConfig,
+ type SubscriberArgs,
+} from "@medusajs/framework"
+import { createStoreWorkflow } from "../workflows/create-store"
+
+export default async function handleUserCreated({
+ event: { data },
+ container,
+}: SubscriberArgs<{ id: string }>) {
+ const { result } = await createStoreWorkflow(container)
+ .run()
+
+ console.log(result)
+}
+
+export const config: SubscriberConfig = {
+ event: "user.created",
+}
+```
+
+### Scheduled Job
+
+```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
+import { MedusaContainer } from "@medusajs/framework/types"
+import { createStoreWorkflow } from "../workflows/create-store"
+
+export default async function myCustomJob(
+ container: MedusaContainer
+) {
+ const { result } = await createStoreWorkflow(container)
+ .run()
+
+ console.log(result)
+}
+
+export const config = {
+ name: "run-once-a-day",
+ schedule: `0 0 * * *`,
+}
+```
+
+Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).
+
+***
+
+
# 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.
@@ -21584,6 +21615,34 @@ The User Module accepts options for further configurations. Refer to [this docum
***
+# 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.
@@ -21682,63 +21741,6 @@ createRemoteLinkStep({
```
-# Links between Currency Module and Other Modules
-
-This document showcases the module links defined between the Currency Module and other Commerce Modules.
-
-## Summary
-
-The Currency Module has the following links to other modules:
-
-Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database.
-
-|First Data Model|Second Data Model|Type|Description|
-|---|---|---|---|
-|StoreCurrency|Currency|Read-only - has one|Learn more|
-
-***
-
-## Store Module
-
-The Store Module has a `Currency` data model that stores the supported currencies of a store. However, these currencies don't hold all the details of a currency, such as its name or symbol.
-
-Instead, Medusa defines a read-only link between the [Store Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/store/index.html.md)'s `StoreCurrency` data model and the Currency Module's `Currency` data model. Because the link is read-only from the `Store`'s side, you can only retrieve the details of a store's supported currencies, and not the other way around.
-
-### Retrieve with Query
-
-To retrieve the details of a store's currencies with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `supported_currencies.currency.*` in `fields`:
-
-### query.graph
-
-```ts
-const { data: stores } = await query.graph({
- entity: "store",
- fields: [
- "supported_currencies.currency.*",
- ],
-})
-
-// stores[0].supported_currencies[0].currency
-```
-
-### useQueryGraphStep
-
-```ts
-import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
-
-// ...
-
-const { data: stores } = useQueryGraphStep({
- entity: "store",
- fields: [
- "supported_currencies.currency.*",
- ],
-})
-
-// stores[0].supported_currencies[0].currency
-```
-
-
# Auth Module Provider
In this guide, you’ll learn about the Auth Module Provider and how it's used.
@@ -21791,34 +21793,6 @@ module.exports = defineConfig({
When you specify the `authMethodsPerActor` configuration, it overrides the default. So, if you don't specify any providers for an actor type, users of that actor type can't authenticate with any provider.
-# API Key Concepts
-
-In this document, you’ll learn about the different types of API keys, their expiration and verification.
-
-## API Key Types
-
-There are two types of API keys:
-
-- `publishable`: A public key used in client applications, such as a storefront.
-- `secret`: A secret key used for authentication and verification purposes, such as an admin user’s authentication token or a password reset token.
-
-The API key’s type is stored in the `type` property of the [ApiKey data model](https://docs.medusajs.com/references/api-key/models/ApiKey/index.html.md).
-
-***
-
-## API Key Expiration
-
-An API key expires when it’s revoked using the [revoke method of the module’s main service](https://docs.medusajs.com/references/api-key/revoke/index.html.md).
-
-The associated token is no longer usable or verifiable.
-
-***
-
-## Token Verification
-
-To verify a token received as an input or in a request, use the [authenticate method of the module’s main service](https://docs.medusajs.com/references/api-key/authenticate/index.html.md) which validates the token against all non-expired tokens.
-
-
# Auth Identity and Actor Types
In this document, you’ll learn about concepts related to identity and actors in the Auth Module.
@@ -22467,6 +22441,1547 @@ 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|
+|---|---|---|---|
+|Customer|AccountHolder|Stored - many-to-many|Learn more|
+|Cart|Customer|Read-only - has one|Learn more|
+|Order|Customer|Read-only - has one|Learn more|
+
+***
+
+## Payment Module
+
+Medusa defines a link between the `Customer` and `AccountHolder` data models, allowing payment providers to save payment methods for a customer, if the payment provider supports it.
+
+This link is available starting from Medusa `v2.5.0`.
+
+### Retrieve with Query
+
+To retrieve the account holder associated with a customer with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`:
+
+### query.graph
+
+```ts
+const { data: customers } = await query.graph({
+ entity: "customer",
+ fields: [
+ "account_holder_link.account_holder.*",
+ ],
+})
+
+// customers[0].account_holder_link?.[0]?.account_holder
+```
+
+### useQueryGraphStep
+
+```ts
+import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
+
+// ...
+
+const { data: customers } = useQueryGraphStep({
+ entity: "customer",
+ fields: [
+ "account_holder_link.account_holder.*",
+ ],
+})
+
+// customers[0].account_holder_link?.[0]?.account_holder
+```
+
+### Manage with Link
+
+To manage the account holders of a customer, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):
+
+### link.create
+
+```ts
+import { Modules } from "@medusajs/framework/utils"
+
+// ...
+
+await link.create({
+ [Modules.CUSTOMER]: {
+ customer_id: "cus_123",
+ },
+ [Modules.PAYMENT]: {
+ account_holder_id: "acchld_123",
+ },
+})
+```
+
+### createRemoteLinkStep
+
+```ts
+import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"
+
+// ...
+
+createRemoteLinkStep({
+ [Modules.CUSTOMER]: {
+ customer_id: "cus_123",
+ },
+ [Modules.PAYMENT]: {
+ account_holder_id: "acchld_123",
+ },
+})
+```
+
+***
+
+## Cart Module
+
+Medusa defines a read-only link between the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `Cart` data model and the `Customer` data model. Because the link is read-only from the `Cart`'s side, you can only retrieve the customer of a cart, and not the other way around.
+
+### Retrieve with Query
+
+To retrieve the customer of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`:
+
+### query.graph
+
+```ts
+const { data: carts } = await query.graph({
+ entity: "cart",
+ fields: [
+ "customer.*",
+ ],
+})
+
+// carts.customer
+```
+
+### useQueryGraphStep
+
+```ts
+import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
+
+// ...
+
+const { data: carts } = useQueryGraphStep({
+ entity: "cart",
+ fields: [
+ "customer.*",
+ ],
+})
+
+// carts.customer
+```
+
+***
+
+## Order Module
+
+Medusa defines a read-only link between the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `Order` data model and the `Customer` data model. Because the link is read-only from the `Order`'s side, you can only retrieve the customer of an order, and not the other way around.
+
+### Retrieve with Query
+
+To retrieve the customer of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`:
+
+### query.graph
+
+```ts
+const { data: orders } = await query.graph({
+ entity: "order",
+ fields: [
+ "customer.*",
+ ],
+})
+
+// orders.customer
+```
+
+### useQueryGraphStep
+
+```ts
+import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
+
+// ...
+
+const { data: orders } = useQueryGraphStep({
+ entity: "order",
+ fields: [
+ "customer.*",
+ ],
+})
+
+// orders.customer
+```
+
+
+# Fulfillment Concepts
+
+In this document, you’ll learn about some basic fulfillment concepts.
+
+## Fulfillment Set
+
+A fulfillment set is a general form or way of fulfillment. For example, shipping is a form of fulfillment, and pick-up is another form of fulfillment. Each of these can be created as fulfillment sets.
+
+A fulfillment set is represented by the [FulfillmentSet data model](https://docs.medusajs.com/references/fulfillment/models/FulfillmentSet/index.html.md). All other configurations, options, and management features are related to a fulfillment set, in one way or another.
+
+```ts
+const fulfillmentSets = await fulfillmentModuleService.createFulfillmentSets(
+ [
+ {
+ name: "Shipping",
+ type: "shipping",
+ },
+ {
+ name: "Pick-up",
+ type: "pick-up",
+ },
+ ]
+)
+```
+
+***
+
+## Service Zone
+
+A service zone is a collection of geographical zones or areas. It’s used to restrict available shipping options to a defined set of locations.
+
+A service zone is represented by the [ServiceZone data model](https://docs.medusajs.com/references/fulfillment/models/ServiceZone/index.html.md). It’s associated with a fulfillment set, as each service zone is specific to a form of fulfillment. For example, if a customer chooses to pick up items, you can restrict the available shipping options based on their location.
+
+
+
+A service zone can have multiple geographical zones, each represented by the [GeoZone data model](https://docs.medusajs.com/references/fulfillment/models/GeoZone/index.html.md). It holds location-related details to narrow down supported areas, such as country, city, or province code.
+
+The province code is always in lower-case and in [ISO 3166-2 format](https://en.wikipedia.org/wiki/ISO_3166-2).
+
+***
+
+## Shipping Profile
+
+A shipping profile defines a type of items that are shipped in a similar manner. For example, a `default` shipping profile is used for all item types, but the `digital` shipping profile is used for digital items that aren’t shipped and delivered conventionally.
+
+A shipping profile is represented by the [ShippingProfile data model](https://docs.medusajs.com/references/fulfillment/models/ShippingProfile/index.html.md). It only defines the profile’s details, but it’s associated with the shipping options available for the item type.
+
+
+# Fulfillment Module Provider
+
+In this guide, you’ll learn about the Fulfillment Module Provider and how it's used.
+
+Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/locations#manage-fulfillment-providers/index.html.md) to learn how to add a fulfillment provider to a location using the dashboard.
+
+## What is a Fulfillment Module Provider?
+
+A Fulfillment Module Provider handles fulfilling items, typically using a third-party integration.
+
+Fulfillment Module Providers registered in the Fulfillment Module's [options](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/module-options/index.html.md) are stored and represented by the [FulfillmentProvider data model](https://docs.medusajs.com/references/fulfillment/models/FulfillmentProvider/index.html.md).
+
+
+
+***
+
+## Default Fulfillment Provider
+
+Medusa provides a Manual Fulfillment Provider that acts as a placeholder fulfillment provider. It doesn't process fulfillment and delegates that to the merchant.
+
+This provider is installed by default in your application and you can use it to fulfill items manually.
+
+The identifier of the manual fulfillment provider is `fp_manual_manual`.
+
+***
+
+## How to Create a Custom Fulfillment Provider?
+
+A Fulfillment Module Provider is a module whose service implements the `IFulfillmentProvider` imported from `@medusajs/framework/types`.
+
+The module can have multiple fulfillment provider services, where each are registered as separate fulfillment providers.
+
+Refer to the [Create Fulfillment Module Provider](https://docs.medusajs.com/references/fulfillment/provider/index.html.md) guide to learn how to create a Fulfillment Module Provider.
+
+{/* TODO add link to user guide */}
+
+After you create a fulfillment provider, you can choose it as the default Fulfillment Module Provider for a stock location in the Medusa Admin dashboard.
+
+***
+
+## How are Fulfillment Providers Registered?
+
+### Configure Fulfillment Module's Providers
+
+The Fulfillment Module accepts a `providers` option that allows you to configure the providers registered in your application.
+
+Learn more about this option in the [Module Options](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/module-options/index.html.md) guide.
+
+### Registration on Application Start
+
+When the Medusa application starts, it registers the Fulfillment Module Providers defined in the `providers` option of the Fulfillment Module.
+
+For each Fulfillment Module Provider, the Medusa application finds all fulfillment provider services defined in them to register.
+
+### FulfillmentProvider Data Model
+
+A registered fulfillment provider is represented by the [FulfillmentProvider data model](https://docs.medusajs.com/references/fulfillment/models/FulfillmentProvider/index.html.md) in the Medusa application.
+
+This data model is used to reference a service in the Fulfillment Module Provider and determine whether it's installed in the application.
+
+
+
+The `FulfillmentProvider` data model has the following properties:
+
+- `id`: The unique identifier of the fulfillment provider. The ID's format is `fp_{identifier}_{id}`, where:
+ - `identifier` is the value of the `identifier` property in the Fulfillment Module Provider's service.
+ - `id` is the value of the `id` property of the Fulfillment Module Provider in `medusa-config.ts`.
+- `is_enabled`: A boolean indicating whether the fulfillment provider is enabled.
+
+### How to Remove a Fulfillment Provider?
+
+You can remove a registered fulfillment provider from the Medusa application by removing it from the `providers` option in the Fulfillment Module's configuration.
+
+Then, the next time the Medusa application starts, it will set the `is_enabled` property of the `FulfillmentProvider`'s record to `false`. This allows you to re-enable the fulfillment provider later if needed by adding it back to the `providers` option.
+
+
+# Item Fulfillment
+
+In this document, you’ll learn about the concepts of item fulfillment.
+
+## Fulfillment Data Model
+
+A fulfillment is the shipping and delivery of one or more items to the customer. It’s represented by the [Fulfillment data model](https://docs.medusajs.com/references/fulfillment/models/Fulfillment/index.html.md).
+
+***
+
+## Fulfillment Processing by a Fulfillment Provider
+
+A fulfillment is associated with a fulfillment provider that handles all its processing, such as creating a shipment for the fulfillment’s items.
+
+The fulfillment is also associated with a shipping option of that provider, which determines how the item is shipped.
+
+
+
+***
+
+## data Property
+
+The `Fulfillment` data model has a `data` property that holds any necessary data for the third-party fulfillment provider to process the fulfillment.
+
+For example, the `data` property can hold the ID of the fulfillment in the third-party provider. The associated fulfillment provider then uses it whenever it retrieves the fulfillment’s details.
+
+***
+
+## Fulfillment Items
+
+A fulfillment is used to fulfill one or more items. Each item is represented by the `FulfillmentItem` data model.
+
+The fulfillment item holds details relevant to fulfilling the item, such as barcode, SKU, and quantity to fulfill.
+
+
+
+***
+
+## Fulfillment Label
+
+Once a shipment is created for the fulfillment, you can store its tracking number, URL, or other related details as a label, represented by the `FulfillmentLabel` data model.
+
+***
+
+## Fulfillment Status
+
+The `Fulfillment` data model has three properties to keep track of the current status of the fulfillment:
+
+- `packed_at`: The date the fulfillment was packed. If set, then the fulfillment has been packed.
+- `shipped_at`: The date the fulfillment was shipped. If set, then the fulfillment has been shipped.
+- `delivered_at`: The date the fulfillment was delivered. If set, then the fulfillment has been delivered.
+
+
+# Links between Fulfillment Module and Other Modules
+
+This document showcases the module links defined between the Fulfillment Module and other Commerce Modules.
+
+## Summary
+
+The Fulfillment Module has the following links to other modules:
+
+|First Data Model|Second Data Model|Type|Description|
+|---|---|---|---|
+|Order|Fulfillment|Stored - one-to-many|Learn more|
+|Return|Fulfillment|Stored - one-to-many|Learn more|
+|PriceSet|ShippingOption|Stored - many-to-one|Learn more|
+|Product|ShippingProfile|Stored - many-to-one|Learn more|
+|StockLocation|FulfillmentProvider|Stored - one-to-many|Learn more|
+|StockLocation|FulfillmentSet|Stored - one-to-many|Learn more|
+
+***
+
+## Order Module
+
+The [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md) provides order-management functionalities.
+
+Medusa defines a link between the `Fulfillment` and `Order` data models. A fulfillment is created for an orders' items.
+
+
+
+A fulfillment is also created for a return's items. So, Medusa defines a link between the `Fulfillment` and `Return` data models.
+
+
+
+### Retrieve with Query
+
+To retrieve the order of a fulfillment with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `order.*` in `fields`:
+
+To retrieve the return, pass `return.*` in `fields`.
+
+### query.graph
+
+```ts
+const { data: fulfillments } = await query.graph({
+ entity: "fulfillment",
+ fields: [
+ "order.*",
+ ],
+})
+
+// fulfillments.order
+```
+
+### useQueryGraphStep
+
+```ts
+import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
+
+// ...
+
+const { data: fulfillments } = useQueryGraphStep({
+ entity: "fulfillment",
+ fields: [
+ "order.*",
+ ],
+})
+
+// fulfillments.order
+```
+
+### Manage with Link
+
+To manage the order of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):
+
+### link.create
+
+```ts
+import { Modules } from "@medusajs/framework/utils"
+
+// ...
+
+await link.create({
+ [Modules.ORDER]: {
+ order_id: "order_123",
+ },
+ [Modules.FULFILLMENT]: {
+ fulfillment_id: "ful_123",
+ },
+})
+```
+
+### createRemoteLinkStep
+
+```ts
+import { Modules } from "@medusajs/framework/utils"
+import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"
+
+// ...
+
+createRemoteLinkStep({
+ [Modules.ORDER]: {
+ order_id: "order_123",
+ },
+ [Modules.FULFILLMENT]: {
+ fulfillment_id: "ful_123",
+ },
+})
+```
+
+***
+
+## Pricing Module
+
+The Pricing Module provides features to store, manage, and retrieve the best prices in a specified context.
+
+Medusa defines a link between the `PriceSet` and `ShippingOption` data models. A shipping option's price is stored as a price set.
+
+
+
+### Retrieve with Query
+
+To retrieve the price set of a shipping option with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `price_set.*` in `fields`:
+
+### query.graph
+
+```ts
+const { data: shippingOptions } = await query.graph({
+ entity: "shipping_option",
+ fields: [
+ "price_set_link.*",
+ ],
+})
+
+// shippingOptions[0].price_set_link?.price_set_id
+```
+
+### useQueryGraphStep
+
+```ts
+import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
+
+// ...
+
+const { data: shippingOptions } = useQueryGraphStep({
+ entity: "shipping_option",
+ fields: [
+ "price_set_link.*",
+ ],
+})
+
+// shippingOptions[0].price_set_link?.price_set_id
+```
+
+### Manage with Link
+
+To manage the price set of a shipping option, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):
+
+### link.create
+
+```ts
+import { Modules } from "@medusajs/framework/utils"
+
+// ...
+
+await link.create({
+ [Modules.FULFILLMENT]: {
+ shipping_option_id: "so_123",
+ },
+ [Modules.PRICING]: {
+ price_set_id: "pset_123",
+ },
+})
+```
+
+### createRemoteLinkStep
+
+```ts
+import { Modules } from "@medusajs/framework/utils"
+import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"
+
+// ...
+
+createRemoteLinkStep({
+ [Modules.FULFILLMENT]: {
+ shipping_option_id: "so_123",
+ },
+ [Modules.PRICING]: {
+ price_set_id: "pset_123",
+ },
+})
+```
+
+***
+
+## Product Module
+
+Medusa defines a link between the `ShippingProfile` data model and the `Product` data model of the Product Module. Each product must belong to a shipping profile.
+
+This link is introduced in [Medusa v2.5.0](https://github.com/medusajs/medusa/releases/tag/v2.5.0).
+
+### Retrieve with Query
+
+To retrieve the products of a shipping profile with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `products.*` in `fields`:
+
+### query.graph
+
+```ts
+const { data: shippingProfiles } = await query.graph({
+ entity: "shipping_profile",
+ fields: [
+ "products.*",
+ ],
+})
+
+// shippingProfiles[0].products
+```
+
+### useQueryGraphStep
+
+```ts
+import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
+
+// ...
+
+const { data: shippingProfiles } = useQueryGraphStep({
+ entity: "shipping_profile",
+ fields: [
+ "products.*",
+ ],
+})
+
+// shippingProfiles[0].products
+```
+
+### Manage with Link
+
+To manage the shipping profile of a product, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):
+
+### link.create
+
+```ts
+import { Modules } from "@medusajs/framework/utils"
+
+// ...
+
+await link.create({
+ [Modules.PRODUCT]: {
+ product_id: "prod_123",
+ },
+ [Modules.FULFILLMENT]: {
+ shipping_profile_id: "sp_123",
+ },
+})
+```
+
+### createRemoteLinkStep
+
+```ts
+import { Modules } from "@medusajs/framework/utils"
+import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"
+
+// ...
+
+createRemoteLinkStep({
+ [Modules.PRODUCT]: {
+ product_id: "prod_123",
+ },
+ [Modules.FULFILLMENT]: {
+ shipping_profile_id: "sp_123",
+ },
+})
+```
+
+***
+
+## Stock Location Module
+
+The Stock Location Module provides features to manage stock locations in a store.
+
+Medusa defines a link between the `FulfillmentSet` and `StockLocation` data models. A fulfillment set can be conditioned to a specific stock location.
+
+
+
+Medusa also defines a link between the `FulfillmentProvider` and `StockLocation` data models to indicate the providers that can be used in a location.
+
+
+
+### Retrieve with Query
+
+To retrieve the stock location of a fulfillment set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `location.*` in `fields`:
+
+To retrieve the stock location of a fulfillment provider, pass `locations.*` in `fields`.
+
+### query.graph
+
+```ts
+const { data: fulfillmentSets } = await query.graph({
+ entity: "fulfillment_set",
+ fields: [
+ "location.*",
+ ],
+})
+
+// fulfillmentSets[0].location
+```
+
+### useQueryGraphStep
+
+```ts
+import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
+
+// ...
+
+const { data: fulfillmentSets } = useQueryGraphStep({
+ entity: "fulfillment_set",
+ fields: [
+ "location.*",
+ ],
+})
+
+// fulfillmentSets[0].location
+```
+
+### Manage with Link
+
+To manage the stock location of a fulfillment set, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):
+
+### link.create
+
+```ts
+import { Modules } from "@medusajs/framework/utils"
+
+// ...
+
+await link.create({
+ [Modules.STOCK_LOCATION]: {
+ stock_location_id: "sloc_123",
+ },
+ [Modules.FULFILLMENT]: {
+ fulfillment_set_id: "fset_123",
+ },
+})
+```
+
+### createRemoteLinkStep
+
+```ts
+import { Modules } from "@medusajs/framework/utils"
+import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"
+
+// ...
+
+createRemoteLinkStep({
+ [Modules.STOCK_LOCATION]: {
+ stock_location_id: "sloc_123",
+ },
+ [Modules.FULFILLMENT]: {
+ fulfillment_set_id: "fset_123",
+ },
+})
+```
+
+
+# Fulfillment Module Options
+
+In this document, you'll learn about the options of the Fulfillment Module.
+
+## providers
+
+The `providers` option is an array of fulfillment module providers.
+
+When the Medusa application starts, these providers are registered and can be used to process fulfillments.
+
+For example:
+
+```ts title="medusa-config.ts"
+import { Modules } from "@medusajs/framework/utils"
+
+// ...
+
+module.exports = defineConfig({
+ // ...
+ modules: [
+ {
+ resolve: "@medusajs/medusa/fulfillment",
+ options: {
+ providers: [
+ {
+ resolve: `@medusajs/medusa/fulfillment-manual`,
+ id: "manual",
+ options: {
+ // provider options...
+ },
+ },
+ ],
+ },
+ },
+ ],
+})
+```
+
+The `providers` option is an array of objects that accept the following properties:
+
+- `resolve`: A string indicating either the package name of the module provider or the path to it relative to the `src` directory.
+- `id`: A string indicating the provider's unique name or ID.
+- `options`: An optional object of the module provider's options.
+
+
+# Shipping Option
+
+In this document, you’ll learn about shipping options and their rules.
+
+## What’s a Shipping Option?
+
+A shipping option is a way of shipping an item. Each fulfillment provider provides a set of shipping options. For example, a provider may provide a shipping option for express shipping and another for standard shipping.
+
+When the customer places their order, they choose a shipping option to be used to fulfill their items.
+
+A shipping option is represented by the [ShippingOption data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOption/index.html.md).
+
+***
+
+## Service Zone Restrictions
+
+A shipping option is restricted by a service zone, limiting the locations a shipping option be used in.
+
+For example, a fulfillment provider may have a shipping option that can be used in the United States, and another in Canada.
+
+
+
+Service zones can be more restrictive, such as restricting to certain cities or province codes.
+
+The province code is always in lower-case and in [ISO 3166-2 format](https://en.wikipedia.org/wiki/ISO_3166-2).
+
+
+
+***
+
+## Shipping Option Rules
+
+You can restrict shipping options by custom rules, such as the item’s weight or the customer’s group.
+
+You can also restrict a shipping option's price based on specific conditions. For example, you can make a shipping option's price free based on the cart's total. Learn more in the Pricing Module's [Price Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-rules#how-to-set-rules-on-a-price/index.html.md) guide.
+
+These rules are represented by the [ShippingOptionRule data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOptionRule/index.html.md). Its properties define the custom rule:
+
+- `attribute`: The name of a property or table that the rule applies to. For example, `customer_group`.
+- `operator`: The operator used in the condition. For example:
+ - To allow multiple values, use the operator `in`, which validates that the provided values are in the rule’s values.
+ - To create a negation condition that considers `value` against the rule, use `nin`, which validates that the provided values aren’t in the rule’s values.
+- `value`: One or more values.
+
+
+
+A shipping option can have multiple rules. For example, you can add rules to a shipping option so that it's available if the customer belongs to the VIP group and the total weight is less than 2000g.
+
+
+
+***
+
+## Shipping Profile and Types
+
+A shipping option belongs to a type. For example, a shipping option’s type may be `express`, while another `standard`. The type is represented by the [ShippingOptionType data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOptionType/index.html.md).
+
+A shipping option also belongs to a shipping profile, as each shipping profile defines the type of items to be shipped in a similar manner.
+
+***
+
+## data Property
+
+When fulfilling an item, you might use a third-party fulfillment provider that requires additional custom data to be passed along from the checkout or order-creation process.
+
+The `ShippingOption` data model has a `data` property. It's an object that stores custom data relevant later when creating and processing a fulfillment.
+
+
+# Inventory Concepts
+
+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.
+
+
+
+### 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.
+
+
+
+### 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
+
+While inventory kits support bundled products, some features like custom pricing for a bundle or separate fulfillment for a bundle's items are not supported. To support those features, follow the [Bundled Products](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/recipes/bundled-products/examples/standard/index.html.md) tutorial to learn how to customize the Medusa application to add 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.
+
+
+
+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.
+
+
+
+### 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)
+
+
+
+***
+
+## 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)
+
+
+
+***
+
+## 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)
+
+
+
+***
+
+## 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)
+
+
+
+***
+
+## 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)
+
+
+
+### 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|
+|---|---|---|---|
+|ProductVariant|InventoryItem|Stored - many-to-many|Learn more|
+|InventoryLevel|StockLocation|Read-only - has many|Learn more|
+
+***
+
+## Product Module
+
+Each product variant has different inventory details. Medusa defines a link between the `ProductVariant` and `InventoryItem` data models.
+
+
+
+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[0].variants
+```
+
+### useQueryGraphStep
+
+```ts
+import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
+
+// ...
+
+const { data: inventoryItems } = useQueryGraphStep({
+ entity: "inventory_item",
+ fields: [
+ "variants.*",
+ ],
+})
+
+// inventoryItems[0].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[0].stock_locations
+```
+
+### useQueryGraphStep
+
+```ts
+import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
+
+// ...
+
+const { data: inventoryLevels } = useQueryGraphStep({
+ entity: "inventory_level",
+ fields: [
+ "stock_locations.*",
+ ],
+})
+
+// inventoryLevels[0].stock_locations
+```
+
+
+# Links between Currency Module and Other Modules
+
+This document showcases the module links defined between the Currency Module and other Commerce Modules.
+
+## Summary
+
+The Currency Module has the following links to other modules:
+
+Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database.
+
+|First Data Model|Second Data Model|Type|Description|
+|---|---|---|---|
+|StoreCurrency|Currency|Read-only - has one|Learn more|
+
+***
+
+## Store Module
+
+The Store Module has a `Currency` data model that stores the supported currencies of a store. However, these currencies don't hold all the details of a currency, such as its name or symbol.
+
+Instead, Medusa defines a read-only link between the [Store Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/store/index.html.md)'s `StoreCurrency` data model and the Currency Module's `Currency` data model. Because the link is read-only from the `Store`'s side, you can only retrieve the details of a store's supported currencies, and not the other way around.
+
+### Retrieve with Query
+
+To retrieve the details of a store's currencies with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `supported_currencies.currency.*` in `fields`:
+
+### query.graph
+
+```ts
+const { data: stores } = await query.graph({
+ entity: "store",
+ fields: [
+ "supported_currencies.currency.*",
+ ],
+})
+
+// stores[0].supported_currencies[0].currency
+```
+
+### useQueryGraphStep
+
+```ts
+import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
+
+// ...
+
+const { data: stores } = useQueryGraphStep({
+ entity: "store",
+ fields: [
+ "supported_currencies.currency.*",
+ ],
+})
+
+// stores[0].supported_currencies[0].currency
+```
+
+
# Cart Concepts
In this document, you’ll get an overview of the main concepts of a cart.
@@ -23136,6 +24651,112 @@ await cartModuleService.setLineItemTaxLines(
```
+# 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.
+
+
+
+### 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).
+
+
+
+### 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 Edit
+
+In this document, you'll learn about order edits.
+
+Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/edit/index.html.md) to learn how to edit an order's items using the dashboard.
+
+## What is an Order Edit?
+
+A merchant can edit an order to add new items or change the quantity of existing items in the order.
+
+An order edit is represented by the [OrderChange data model](https://docs.medusajs.com/references/order/models/OrderChange/index.html.md).
+
+The `OrderChange` data model is associated with any type of change, including a return or exchange. However, its `change_type` property distinguishes the type of change it's making.
+
+In the case of an order edit, the `OrderChange`'s type is `edit`.
+
+***
+
+## Add Items in an Order Edit
+
+When the merchant adds new items to the order in the order edit, the item is added as an [OrderItem](https://docs.medusajs.com/references/order/models/OrderItem/index.html.md).
+
+Also, an `OrderChangeAction` is created. The [OrderChangeAction data model](https://docs.medusajs.com/references/order/models/OrderChangeAction/index.html.md) represents a change made by an `OrderChange`, such as an item added.
+
+So, when an item is added, an `OrderChangeAction` is created with the type `ITEM_ADD`. In its `details` property, the item's ID, price, and quantity are stored.
+
+***
+
+## Update Items in an Order Edit
+
+A merchant can update an existing item's quantity or price.
+
+This change is added as an `OrderChangeAction` with the type `ITEM_UPDATE`. In its `details` property, the item's ID, new price, and new quantity are stored.
+
+***
+
+## Shipping Methods of New Items in the Edit
+
+Adding new items to the order requires adding shipping methods for those items.
+
+These shipping methods are represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderItem/index.html.md). Also, an `OrderChangeAction` is created with the type `SHIPPING_ADD`
+
+***
+
+## How Order Edits Impact an Order’s Version
+
+When an order edit is confirmed, the order’s version is incremented.
+
+***
+
+## Payments and Refunds for Order Edit Changes
+
+Once the Order Edit is confirmed, any additional payment or refund required can be made on the original order.
+
+This is determined by the comparison between the `OrderSummary` and the order's transactions, as mentioned in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/transactions#checking-outstanding-amount/index.html.md).
+
+
# 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.
@@ -23682,961 +25303,6 @@ If the authentication is successful, the request returns an object with a `succe
```
-# Fulfillment Concepts
-
-In this document, you’ll learn about some basic fulfillment concepts.
-
-## Fulfillment Set
-
-A fulfillment set is a general form or way of fulfillment. For example, shipping is a form of fulfillment, and pick-up is another form of fulfillment. Each of these can be created as fulfillment sets.
-
-A fulfillment set is represented by the [FulfillmentSet data model](https://docs.medusajs.com/references/fulfillment/models/FulfillmentSet/index.html.md). All other configurations, options, and management features are related to a fulfillment set, in one way or another.
-
-```ts
-const fulfillmentSets = await fulfillmentModuleService.createFulfillmentSets(
- [
- {
- name: "Shipping",
- type: "shipping",
- },
- {
- name: "Pick-up",
- type: "pick-up",
- },
- ]
-)
-```
-
-***
-
-## Service Zone
-
-A service zone is a collection of geographical zones or areas. It’s used to restrict available shipping options to a defined set of locations.
-
-A service zone is represented by the [ServiceZone data model](https://docs.medusajs.com/references/fulfillment/models/ServiceZone/index.html.md). It’s associated with a fulfillment set, as each service zone is specific to a form of fulfillment. For example, if a customer chooses to pick up items, you can restrict the available shipping options based on their location.
-
-
-
-A service zone can have multiple geographical zones, each represented by the [GeoZone data model](https://docs.medusajs.com/references/fulfillment/models/GeoZone/index.html.md). It holds location-related details to narrow down supported areas, such as country, city, or province code.
-
-The province code is always in lower-case and in [ISO 3166-2 format](https://en.wikipedia.org/wiki/ISO_3166-2).
-
-***
-
-## Shipping Profile
-
-A shipping profile defines a type of items that are shipped in a similar manner. For example, a `default` shipping profile is used for all item types, but the `digital` shipping profile is used for digital items that aren’t shipped and delivered conventionally.
-
-A shipping profile is represented by the [ShippingProfile data model](https://docs.medusajs.com/references/fulfillment/models/ShippingProfile/index.html.md). It only defines the profile’s details, but it’s associated with the shipping options available for the item type.
-
-
-# Item Fulfillment
-
-In this document, you’ll learn about the concepts of item fulfillment.
-
-## Fulfillment Data Model
-
-A fulfillment is the shipping and delivery of one or more items to the customer. It’s represented by the [Fulfillment data model](https://docs.medusajs.com/references/fulfillment/models/Fulfillment/index.html.md).
-
-***
-
-## Fulfillment Processing by a Fulfillment Provider
-
-A fulfillment is associated with a fulfillment provider that handles all its processing, such as creating a shipment for the fulfillment’s items.
-
-The fulfillment is also associated with a shipping option of that provider, which determines how the item is shipped.
-
-
-
-***
-
-## data Property
-
-The `Fulfillment` data model has a `data` property that holds any necessary data for the third-party fulfillment provider to process the fulfillment.
-
-For example, the `data` property can hold the ID of the fulfillment in the third-party provider. The associated fulfillment provider then uses it whenever it retrieves the fulfillment’s details.
-
-***
-
-## Fulfillment Items
-
-A fulfillment is used to fulfill one or more items. Each item is represented by the `FulfillmentItem` data model.
-
-The fulfillment item holds details relevant to fulfilling the item, such as barcode, SKU, and quantity to fulfill.
-
-
-
-***
-
-## Fulfillment Label
-
-Once a shipment is created for the fulfillment, you can store its tracking number, URL, or other related details as a label, represented by the `FulfillmentLabel` data model.
-
-***
-
-## Fulfillment Status
-
-The `Fulfillment` data model has three properties to keep track of the current status of the fulfillment:
-
-- `packed_at`: The date the fulfillment was packed. If set, then the fulfillment has been packed.
-- `shipped_at`: The date the fulfillment was shipped. If set, then the fulfillment has been shipped.
-- `delivered_at`: The date the fulfillment was delivered. If set, then the fulfillment has been delivered.
-
-
-# Fulfillment Module Provider
-
-In this guide, you’ll learn about the Fulfillment Module Provider and how it's used.
-
-Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/locations#manage-fulfillment-providers/index.html.md) to learn how to add a fulfillment provider to a location using the dashboard.
-
-## What is a Fulfillment Module Provider?
-
-A Fulfillment Module Provider handles fulfilling items, typically using a third-party integration.
-
-Fulfillment Module Providers registered in the Fulfillment Module's [options](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/module-options/index.html.md) are stored and represented by the [FulfillmentProvider data model](https://docs.medusajs.com/references/fulfillment/models/FulfillmentProvider/index.html.md).
-
-
-
-***
-
-## Default Fulfillment Provider
-
-Medusa provides a Manual Fulfillment Provider that acts as a placeholder fulfillment provider. It doesn't process fulfillment and delegates that to the merchant.
-
-This provider is installed by default in your application and you can use it to fulfill items manually.
-
-The identifier of the manual fulfillment provider is `fp_manual_manual`.
-
-***
-
-## How to Create a Custom Fulfillment Provider?
-
-A Fulfillment Module Provider is a module whose service implements the `IFulfillmentProvider` imported from `@medusajs/framework/types`.
-
-The module can have multiple fulfillment provider services, where each are registered as separate fulfillment providers.
-
-Refer to the [Create Fulfillment Module Provider](https://docs.medusajs.com/references/fulfillment/provider/index.html.md) guide to learn how to create a Fulfillment Module Provider.
-
-{/* TODO add link to user guide */}
-
-After you create a fulfillment provider, you can choose it as the default Fulfillment Module Provider for a stock location in the Medusa Admin dashboard.
-
-***
-
-## How are Fulfillment Providers Registered?
-
-### Configure Fulfillment Module's Providers
-
-The Fulfillment Module accepts a `providers` option that allows you to configure the providers registered in your application.
-
-Learn more about this option in the [Module Options](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/module-options/index.html.md) guide.
-
-### Registration on Application Start
-
-When the Medusa application starts, it registers the Fulfillment Module Providers defined in the `providers` option of the Fulfillment Module.
-
-For each Fulfillment Module Provider, the Medusa application finds all fulfillment provider services defined in them to register.
-
-### FulfillmentProvider Data Model
-
-A registered fulfillment provider is represented by the [FulfillmentProvider data model](https://docs.medusajs.com/references/fulfillment/models/FulfillmentProvider/index.html.md) in the Medusa application.
-
-This data model is used to reference a service in the Fulfillment Module Provider and determine whether it's installed in the application.
-
-
-
-The `FulfillmentProvider` data model has the following properties:
-
-- `id`: The unique identifier of the fulfillment provider. The ID's format is `fp_{identifier}_{id}`, where:
- - `identifier` is the value of the `identifier` property in the Fulfillment Module Provider's service.
- - `id` is the value of the `id` property of the Fulfillment Module Provider in `medusa-config.ts`.
-- `is_enabled`: A boolean indicating whether the fulfillment provider is enabled.
-
-### How to Remove a Fulfillment Provider?
-
-You can remove a registered fulfillment provider from the Medusa application by removing it from the `providers` option in the Fulfillment Module's configuration.
-
-Then, the next time the Medusa application starts, it will set the `is_enabled` property of the `FulfillmentProvider`'s record to `false`. This allows you to re-enable the fulfillment provider later if needed by adding it back to the `providers` option.
-
-
-# Links between Fulfillment Module and Other Modules
-
-This document showcases the module links defined between the Fulfillment Module and other Commerce Modules.
-
-## Summary
-
-The Fulfillment Module has the following links to other modules:
-
-|First Data Model|Second Data Model|Type|Description|
-|---|---|---|---|
-|Order|Fulfillment|Stored - one-to-many|Learn more|
-|Return|Fulfillment|Stored - one-to-many|Learn more|
-|PriceSet|ShippingOption|Stored - many-to-one|Learn more|
-|Product|ShippingProfile|Stored - many-to-one|Learn more|
-|StockLocation|FulfillmentProvider|Stored - one-to-many|Learn more|
-|StockLocation|FulfillmentSet|Stored - one-to-many|Learn more|
-
-***
-
-## Order Module
-
-The [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md) provides order-management functionalities.
-
-Medusa defines a link between the `Fulfillment` and `Order` data models. A fulfillment is created for an orders' items.
-
-
-
-A fulfillment is also created for a return's items. So, Medusa defines a link between the `Fulfillment` and `Return` data models.
-
-
-
-### Retrieve with Query
-
-To retrieve the order of a fulfillment with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `order.*` in `fields`:
-
-To retrieve the return, pass `return.*` in `fields`.
-
-### query.graph
-
-```ts
-const { data: fulfillments } = await query.graph({
- entity: "fulfillment",
- fields: [
- "order.*",
- ],
-})
-
-// fulfillments.order
-```
-
-### useQueryGraphStep
-
-```ts
-import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
-
-// ...
-
-const { data: fulfillments } = useQueryGraphStep({
- entity: "fulfillment",
- fields: [
- "order.*",
- ],
-})
-
-// fulfillments.order
-```
-
-### Manage with Link
-
-To manage the order of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):
-
-### link.create
-
-```ts
-import { Modules } from "@medusajs/framework/utils"
-
-// ...
-
-await link.create({
- [Modules.ORDER]: {
- order_id: "order_123",
- },
- [Modules.FULFILLMENT]: {
- fulfillment_id: "ful_123",
- },
-})
-```
-
-### createRemoteLinkStep
-
-```ts
-import { Modules } from "@medusajs/framework/utils"
-import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"
-
-// ...
-
-createRemoteLinkStep({
- [Modules.ORDER]: {
- order_id: "order_123",
- },
- [Modules.FULFILLMENT]: {
- fulfillment_id: "ful_123",
- },
-})
-```
-
-***
-
-## Pricing Module
-
-The Pricing Module provides features to store, manage, and retrieve the best prices in a specified context.
-
-Medusa defines a link between the `PriceSet` and `ShippingOption` data models. A shipping option's price is stored as a price set.
-
-
-
-### Retrieve with Query
-
-To retrieve the price set of a shipping option with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `price_set.*` in `fields`:
-
-### query.graph
-
-```ts
-const { data: shippingOptions } = await query.graph({
- entity: "shipping_option",
- fields: [
- "price_set_link.*",
- ],
-})
-
-// shippingOptions[0].price_set_link?.price_set_id
-```
-
-### useQueryGraphStep
-
-```ts
-import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
-
-// ...
-
-const { data: shippingOptions } = useQueryGraphStep({
- entity: "shipping_option",
- fields: [
- "price_set_link.*",
- ],
-})
-
-// shippingOptions[0].price_set_link?.price_set_id
-```
-
-### Manage with Link
-
-To manage the price set of a shipping option, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):
-
-### link.create
-
-```ts
-import { Modules } from "@medusajs/framework/utils"
-
-// ...
-
-await link.create({
- [Modules.FULFILLMENT]: {
- shipping_option_id: "so_123",
- },
- [Modules.PRICING]: {
- price_set_id: "pset_123",
- },
-})
-```
-
-### createRemoteLinkStep
-
-```ts
-import { Modules } from "@medusajs/framework/utils"
-import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"
-
-// ...
-
-createRemoteLinkStep({
- [Modules.FULFILLMENT]: {
- shipping_option_id: "so_123",
- },
- [Modules.PRICING]: {
- price_set_id: "pset_123",
- },
-})
-```
-
-***
-
-## Product Module
-
-Medusa defines a link between the `ShippingProfile` data model and the `Product` data model of the Product Module. Each product must belong to a shipping profile.
-
-This link is introduced in [Medusa v2.5.0](https://github.com/medusajs/medusa/releases/tag/v2.5.0).
-
-### Retrieve with Query
-
-To retrieve the products of a shipping profile with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `products.*` in `fields`:
-
-### query.graph
-
-```ts
-const { data: shippingProfiles } = await query.graph({
- entity: "shipping_profile",
- fields: [
- "products.*",
- ],
-})
-
-// shippingProfiles[0].products
-```
-
-### useQueryGraphStep
-
-```ts
-import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
-
-// ...
-
-const { data: shippingProfiles } = useQueryGraphStep({
- entity: "shipping_profile",
- fields: [
- "products.*",
- ],
-})
-
-// shippingProfiles[0].products
-```
-
-### Manage with Link
-
-To manage the shipping profile of a product, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):
-
-### link.create
-
-```ts
-import { Modules } from "@medusajs/framework/utils"
-
-// ...
-
-await link.create({
- [Modules.PRODUCT]: {
- product_id: "prod_123",
- },
- [Modules.FULFILLMENT]: {
- shipping_profile_id: "sp_123",
- },
-})
-```
-
-### createRemoteLinkStep
-
-```ts
-import { Modules } from "@medusajs/framework/utils"
-import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"
-
-// ...
-
-createRemoteLinkStep({
- [Modules.PRODUCT]: {
- product_id: "prod_123",
- },
- [Modules.FULFILLMENT]: {
- shipping_profile_id: "sp_123",
- },
-})
-```
-
-***
-
-## Stock Location Module
-
-The Stock Location Module provides features to manage stock locations in a store.
-
-Medusa defines a link between the `FulfillmentSet` and `StockLocation` data models. A fulfillment set can be conditioned to a specific stock location.
-
-
-
-Medusa also defines a link between the `FulfillmentProvider` and `StockLocation` data models to indicate the providers that can be used in a location.
-
-
-
-### Retrieve with Query
-
-To retrieve the stock location of a fulfillment set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `location.*` in `fields`:
-
-To retrieve the stock location of a fulfillment provider, pass `locations.*` in `fields`.
-
-### query.graph
-
-```ts
-const { data: fulfillmentSets } = await query.graph({
- entity: "fulfillment_set",
- fields: [
- "location.*",
- ],
-})
-
-// fulfillmentSets[0].location
-```
-
-### useQueryGraphStep
-
-```ts
-import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
-
-// ...
-
-const { data: fulfillmentSets } = useQueryGraphStep({
- entity: "fulfillment_set",
- fields: [
- "location.*",
- ],
-})
-
-// fulfillmentSets[0].location
-```
-
-### Manage with Link
-
-To manage the stock location of a fulfillment set, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):
-
-### link.create
-
-```ts
-import { Modules } from "@medusajs/framework/utils"
-
-// ...
-
-await link.create({
- [Modules.STOCK_LOCATION]: {
- stock_location_id: "sloc_123",
- },
- [Modules.FULFILLMENT]: {
- fulfillment_set_id: "fset_123",
- },
-})
-```
-
-### createRemoteLinkStep
-
-```ts
-import { Modules } from "@medusajs/framework/utils"
-import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"
-
-// ...
-
-createRemoteLinkStep({
- [Modules.STOCK_LOCATION]: {
- stock_location_id: "sloc_123",
- },
- [Modules.FULFILLMENT]: {
- fulfillment_set_id: "fset_123",
- },
-})
-```
-
-
-# Fulfillment Module Options
-
-In this document, you'll learn about the options of the Fulfillment Module.
-
-## providers
-
-The `providers` option is an array of fulfillment module providers.
-
-When the Medusa application starts, these providers are registered and can be used to process fulfillments.
-
-For example:
-
-```ts title="medusa-config.ts"
-import { Modules } from "@medusajs/framework/utils"
-
-// ...
-
-module.exports = defineConfig({
- // ...
- modules: [
- {
- resolve: "@medusajs/medusa/fulfillment",
- options: {
- providers: [
- {
- resolve: `@medusajs/medusa/fulfillment-manual`,
- id: "manual",
- options: {
- // provider options...
- },
- },
- ],
- },
- },
- ],
-})
-```
-
-The `providers` option is an array of objects that accept the following properties:
-
-- `resolve`: A string indicating either the package name of the module provider or the path to it relative to the `src` directory.
-- `id`: A string indicating the provider's unique name or ID.
-- `options`: An optional object of the module provider's options.
-
-
-# Shipping Option
-
-In this document, you’ll learn about shipping options and their rules.
-
-## What’s a Shipping Option?
-
-A shipping option is a way of shipping an item. Each fulfillment provider provides a set of shipping options. For example, a provider may provide a shipping option for express shipping and another for standard shipping.
-
-When the customer places their order, they choose a shipping option to be used to fulfill their items.
-
-A shipping option is represented by the [ShippingOption data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOption/index.html.md).
-
-***
-
-## Service Zone Restrictions
-
-A shipping option is restricted by a service zone, limiting the locations a shipping option be used in.
-
-For example, a fulfillment provider may have a shipping option that can be used in the United States, and another in Canada.
-
-
-
-Service zones can be more restrictive, such as restricting to certain cities or province codes.
-
-The province code is always in lower-case and in [ISO 3166-2 format](https://en.wikipedia.org/wiki/ISO_3166-2).
-
-
-
-***
-
-## Shipping Option Rules
-
-You can restrict shipping options by custom rules, such as the item’s weight or the customer’s group.
-
-You can also restrict a shipping option's price based on specific conditions. For example, you can make a shipping option's price free based on the cart's total. Learn more in the Pricing Module's [Price Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-rules#how-to-set-rules-on-a-price/index.html.md) guide.
-
-These rules are represented by the [ShippingOptionRule data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOptionRule/index.html.md). Its properties define the custom rule:
-
-- `attribute`: The name of a property or table that the rule applies to. For example, `customer_group`.
-- `operator`: The operator used in the condition. For example:
- - To allow multiple values, use the operator `in`, which validates that the provided values are in the rule’s values.
- - To create a negation condition that considers `value` against the rule, use `nin`, which validates that the provided values aren’t in the rule’s values.
-- `value`: One or more values.
-
-
-
-A shipping option can have multiple rules. For example, you can add rules to a shipping option so that it's available if the customer belongs to the VIP group and the total weight is less than 2000g.
-
-
-
-***
-
-## Shipping Profile and Types
-
-A shipping option belongs to a type. For example, a shipping option’s type may be `express`, while another `standard`. The type is represented by the [ShippingOptionType data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOptionType/index.html.md).
-
-A shipping option also belongs to a shipping profile, as each shipping profile defines the type of items to be shipped in a similar manner.
-
-***
-
-## data Property
-
-When fulfilling an item, you might use a third-party fulfillment provider that requires additional custom data to be passed along from the checkout or order-creation process.
-
-The `ShippingOption` data model has a `data` property. It's an object that stores custom data relevant later when creating and processing a fulfillment.
-
-
-# Customer Accounts
-
-In this document, you’ll learn how registered and unregistered accounts are distinguished in the Medusa application.
-
-Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/customers/index.html.md) to learn how to manage customers using the dashboard.
-
-## `has_account` Property
-
-The [Customer data model](https://docs.medusajs.com/references/customer/models/Customer/index.html.md) has a `has_account` property, which is a boolean that indicates whether a customer is registered.
-
-When a guest customer places an order, a new `Customer` record is created with `has_account` set to `false`.
-
-When this or another guest customer registers an account with the same email, a new `Customer` record is created with `has_account` set to `true`.
-
-***
-
-## Email Uniqueness
-
-The above behavior means that two `Customer` records may exist with the same email. However, the main difference is the `has_account` property's value.
-
-So, there can only be one guest customer (having `has_account=false`) and one registered customer (having `has_account=true`) with the same email.
-
-
-# Links between Customer Module and Other Modules
-
-This document showcases the module links defined between the Customer Module and other Commerce Modules.
-
-## Summary
-
-The Customer Module has the following links to other modules:
-
-Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database.
-
-|First Data Model|Second Data Model|Type|Description|
-|---|---|---|---|
-|Customer|AccountHolder|Stored - many-to-many|Learn more|
-|Cart|Customer|Read-only - has one|Learn more|
-|Order|Customer|Read-only - has one|Learn more|
-
-***
-
-## Payment Module
-
-Medusa defines a link between the `Customer` and `AccountHolder` data models, allowing payment providers to save payment methods for a customer, if the payment provider supports it.
-
-This link is available starting from Medusa `v2.5.0`.
-
-### Retrieve with Query
-
-To retrieve the account holder associated with a customer with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`:
-
-### query.graph
-
-```ts
-const { data: customers } = await query.graph({
- entity: "customer",
- fields: [
- "account_holder_link.account_holder.*",
- ],
-})
-
-// customers[0].account_holder_link?.[0]?.account_holder
-```
-
-### useQueryGraphStep
-
-```ts
-import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
-
-// ...
-
-const { data: customers } = useQueryGraphStep({
- entity: "customer",
- fields: [
- "account_holder_link.account_holder.*",
- ],
-})
-
-// customers[0].account_holder_link?.[0]?.account_holder
-```
-
-### Manage with Link
-
-To manage the account holders of a customer, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):
-
-### link.create
-
-```ts
-import { Modules } from "@medusajs/framework/utils"
-
-// ...
-
-await link.create({
- [Modules.CUSTOMER]: {
- customer_id: "cus_123",
- },
- [Modules.PAYMENT]: {
- account_holder_id: "acchld_123",
- },
-})
-```
-
-### createRemoteLinkStep
-
-```ts
-import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"
-
-// ...
-
-createRemoteLinkStep({
- [Modules.CUSTOMER]: {
- customer_id: "cus_123",
- },
- [Modules.PAYMENT]: {
- account_holder_id: "acchld_123",
- },
-})
-```
-
-***
-
-## Cart Module
-
-Medusa defines a read-only link between the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `Cart` data model and the `Customer` data model. Because the link is read-only from the `Cart`'s side, you can only retrieve the customer of a cart, and not the other way around.
-
-### Retrieve with Query
-
-To retrieve the customer of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`:
-
-### query.graph
-
-```ts
-const { data: carts } = await query.graph({
- entity: "cart",
- fields: [
- "customer.*",
- ],
-})
-
-// carts.customer
-```
-
-### useQueryGraphStep
-
-```ts
-import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
-
-// ...
-
-const { data: carts } = useQueryGraphStep({
- entity: "cart",
- fields: [
- "customer.*",
- ],
-})
-
-// carts.customer
-```
-
-***
-
-## Order Module
-
-Medusa defines a read-only link between the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `Order` data model and the `Customer` data model. Because the link is read-only from the `Order`'s side, you can only retrieve the customer of an order, and not the other way around.
-
-### Retrieve with Query
-
-To retrieve the customer of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`:
-
-### query.graph
-
-```ts
-const { data: orders } = await query.graph({
- entity: "order",
- fields: [
- "customer.*",
- ],
-})
-
-// orders.customer
-```
-
-### useQueryGraphStep
-
-```ts
-import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
-
-// ...
-
-const { data: orders } = useQueryGraphStep({
- entity: "order",
- fields: [
- "customer.*",
- ],
-})
-
-// orders.customer
-```
-
-
-# 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.
-
-
-
-### 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).
-
-
-
-### 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 Edit
-
-In this document, you'll learn about order edits.
-
-Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/edit/index.html.md) to learn how to edit an order's items using the dashboard.
-
-## What is an Order Edit?
-
-A merchant can edit an order to add new items or change the quantity of existing items in the order.
-
-An order edit is represented by the [OrderChange data model](https://docs.medusajs.com/references/order/models/OrderChange/index.html.md).
-
-The `OrderChange` data model is associated with any type of change, including a return or exchange. However, its `change_type` property distinguishes the type of change it's making.
-
-In the case of an order edit, the `OrderChange`'s type is `edit`.
-
-***
-
-## Add Items in an Order Edit
-
-When the merchant adds new items to the order in the order edit, the item is added as an [OrderItem](https://docs.medusajs.com/references/order/models/OrderItem/index.html.md).
-
-Also, an `OrderChangeAction` is created. The [OrderChangeAction data model](https://docs.medusajs.com/references/order/models/OrderChangeAction/index.html.md) represents a change made by an `OrderChange`, such as an item added.
-
-So, when an item is added, an `OrderChangeAction` is created with the type `ITEM_ADD`. In its `details` property, the item's ID, price, and quantity are stored.
-
-***
-
-## Update Items in an Order Edit
-
-A merchant can update an existing item's quantity or price.
-
-This change is added as an `OrderChangeAction` with the type `ITEM_UPDATE`. In its `details` property, the item's ID, new price, and new quantity are stored.
-
-***
-
-## Shipping Methods of New Items in the Edit
-
-Adding new items to the order requires adding shipping methods for those items.
-
-These shipping methods are represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderItem/index.html.md). Also, an `OrderChangeAction` is created with the type `SHIPPING_ADD`
-
-***
-
-## How Order Edits Impact an Order’s Version
-
-When an order edit is confirmed, the order’s version is incremented.
-
-***
-
-## Payments and Refunds for Order Edit Changes
-
-Once the Order Edit is confirmed, any additional payment or refund required can be made on the original order.
-
-This is determined by the comparison between the `OrderSummary` and the order's transactions, as mentioned in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/transactions#checking-outstanding-amount/index.html.md).
-
-
# Links between Order Module and Other Modules
This document showcases the module links defined between the Order Module and other Commerce Modules.
@@ -25161,6 +25827,36 @@ const { data: orders } = useQueryGraphStep({
```
+# Order Versioning
+
+In this document, you’ll learn how an order and its details are versioned.
+
+## What's Versioning?
+
+Versioning means assigning a version number to a record, such as an order and its items. This is useful to view the different versions of the order following changes in its lifetime.
+
+When changes are made on an order, such as an item is added or returned, the order's version changes.
+
+***
+
+## version Property
+
+The `Order` and `OrderSummary` data models have a `version` property that indicates the current version. By default, its value is `1`.
+
+Other order-related data models, such as `OrderItem`, also has a `version` property, but it indicates the version it belongs to.
+
+***
+
+## How the Version Changes
+
+When the order is changed, such as an item is exchanged, this changes the version of the order and its related data:
+
+1. The version of the order and its summary is incremented.
+2. Related order data that have a `version` property, such as the `OrderItem`, are duplicated. The duplicated item has the new version, whereas the original item has the previous version.
+
+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.
@@ -25200,34 +25896,57 @@ The following table lists the possible `action` values that Medusa uses and what
|\`WRITE\_OFF\_ITEM\`|Remove an item's quantity as part of the claim, without adding the quantity back to the item variant's inventory.|\`details\`|
-# Order Versioning
+# Order Exchange
-In this document, you’ll learn how an order and its details are versioned.
+In this document, you’ll learn about order exchanges.
-## What's Versioning?
+Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/exchanges/index.html.md) to learn how to manage an order's exchanges using the dashboard.
-Versioning means assigning a version number to a record, such as an order and its items. This is useful to view the different versions of the order following changes in its lifetime.
+## What is an Exchange?
-When changes are made on an order, such as an item is added or returned, the order's version changes.
+An exchange is the replacement of an item that the customer ordered with another.
+
+A merchant creates the exchange, specifying the items to be replaced and the new items to be sent.
+
+The [OrderExchange data model](https://docs.medusajs.com/references/order/models/OrderExchange/index.html.md) represents an exchange.
***
-## version Property
+## Returned and New Items
-The `Order` and `OrderSummary` data models have a `version` property that indicates the current version. By default, its value is `1`.
+When the exchange is created, a return, represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md), is created to handle receiving the items back from the customer.
-Other order-related data models, such as `OrderItem`, also has a `version` property, but it indicates the version it belongs to.
+Learn more about returns in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md).
+
+The [OrderExchangeItem data model](https://docs.medusajs.com/references/order/models/OrderExchangeItem/index.html.md) represents the new items to be sent to the customer.
***
-## How the Version Changes
+## Exchange Shipping Methods
-When the order is changed, such as an item is exchanged, this changes the version of the order and its related data:
+An exchange has shipping methods used to send the new items to the customer. They’re represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md).
-1. The version of the order and its summary is incremented.
-2. Related order data that have a `version` property, such as the `OrderItem`, are duplicated. The duplicated item has the new version, whereas the original item has the previous version.
+The shipping methods for the returned items are associated with the exchange's return, as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return#return-shipping-methods/index.html.md).
-When the order is retrieved, only the related data having the same version is retrieved.
+***
+
+## Exchange Payment
+
+The `Exchange` data model has a `difference_due` property that stores the outstanding amount.
+
+|Condition|Result|
+|---|---|---|
+|\`difference\_due \< 0\`|Merchant owes the customer a refund of the |
+|\`difference\_due > 0\`|Merchant requires additional payment from the customer of the |
+|\`difference\_due = 0\`|No payment processing is required.|
+
+Any payment or refund made is stored in the [Transaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md).
+
+***
+
+## How Exchanges Impact an Order’s Version
+
+When an exchange is confirmed, the order’s version is incremented.
# Promotions Adjustments in Orders
@@ -25352,6 +26071,121 @@ await orderModuleService.setOrderShippingMethodAdjustments(
```
+# Order Claim
+
+In this document, you’ll learn about order claims.
+
+Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/claims/index.html.md) to learn how to manage an order's claims using the dashboard.
+
+## What is a Claim?
+
+When a customer receives a defective or incorrect item, the merchant can create a claim to refund or replace the item.
+
+The [OrderClaim data model](https://docs.medusajs.com/references/order/models/OrderClaim/index.html.md) represents a claim.
+
+***
+
+## Claim Type
+
+The `Claim` data model has a `type` property whose value indicates the type of the claim:
+
+- `refund`: the items are returned, and the customer is refunded.
+- `replace`: the items are returned, and the customer receives new items.
+
+***
+
+## Old and Replacement Items
+
+When the claim is created, a return, represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md), is also created to handle receiving the old items from the customer.
+
+Learn more about returns in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md).
+
+If the claim’s type is `replace`, replacement items are represented by the [ClaimItem data model](https://docs.medusajs.com/references/order/models/OrderClaimItem/index.html.md).
+
+***
+
+## Claim Shipping Methods
+
+A claim uses shipping methods to send the replacement items to the customer. These methods are represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md).
+
+The shipping methods for the returned items are associated with the claim's return, as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return#return-shipping-methods/index.html.md).
+
+***
+
+## Claim Refund
+
+If the claim’s type is `refund`, the amount to be refunded is stored in the `refund_amount` property.
+
+The [Transaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md) represents the refunds made for the claim.
+
+***
+
+## How Claims Impact an Order’s Version
+
+When a claim is confirmed, the order’s version is incremented.
+
+
+# Order Return
+
+In this document, you’ll learn about order returns.
+
+Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/returns/index.html.md) to learn how to manage an order's returns using the dashboard.
+
+## What is a Return?
+
+A return is the return of items delivered from the customer back to the merchant. It is represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md).
+
+A return is requested either by the customer from the storefront, or the merchant from the admin. Medusa supports an automated Return Merchandise Authorization (RMA) flow.
+
+
+
+Once the merchant receives the returned items, they mark the return as received.
+
+***
+
+## Returned Items
+
+The items to be returned are represented by the [ReturnItem data model](https://docs.medusajs.com/references/order/models/ReturnItem/index.html.md).
+
+The `ReturnItem` model has two properties storing the item's quantity:
+
+1. `received_quantity`: The quantity of the item that's received and can be added to the item's inventory quantity.
+2. `damaged_quantity`: The quantity of the item that's damaged, meaning it can't be sold again or added to the item's inventory quantity.
+
+***
+
+## Return Shipping Methods
+
+A return has shipping methods used to return the items to the merchant. The shipping methods are represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md).
+
+In the Medusa application, the shipping method for a return is created only from a shipping option, provided by the Fulfillment Module, that has the rule `is_return` enabled.
+
+***
+
+## Refund Payment
+
+The `refund_amount` property of the `Return` data model holds the amount a merchant must refund the customer.
+
+The [OrderTransaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md) represents the refunds made for the return.
+
+***
+
+## Returns in Exchanges and Claims
+
+When a merchant creates an exchange or a claim, it includes returning items from the customer.
+
+The `Return` data model also represents the return of these items. In this case, the return is associated with the exchange or claim it was created for.
+
+***
+
+## How Returns Impact an Order’s Version
+
+The order’s version is incremented when:
+
+1. A return is requested.
+2. A return is marked as received.
+
+
# Tax Lines in Order Module
In this document, you’ll learn about tax lines in an order.
@@ -25429,641 +26263,6 @@ 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`.
-# 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.
-
-
-
-### 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)
-
-
-
-***
-
-## 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)
-
-
-
-***
-
-## 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)
-
-
-
-***
-
-## 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)
-
-
-
-***
-
-## 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)
-
-
-
-### 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.
-
-
-
-### 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
-
-While inventory kits support bundled products, some features like custom pricing for a bundle or separate fulfillment for a bundle's items are not supported. To support those features, follow the [Bundled Products](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/recipes/bundled-products/examples/standard/index.html.md) tutorial to learn how to customize the Medusa application to add 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.
-
-
-
-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.
-
-
-
-### 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|
-|---|---|---|---|
-|ProductVariant|InventoryItem|Stored - many-to-many|Learn more|
-|InventoryLevel|StockLocation|Read-only - has many|Learn more|
-
-***
-
-## Product Module
-
-Each product variant has different inventory details. Medusa defines a link between the `ProductVariant` and `InventoryItem` data models.
-
-
-
-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[0].variants
-```
-
-### useQueryGraphStep
-
-```ts
-import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
-
-// ...
-
-const { data: inventoryItems } = useQueryGraphStep({
- entity: "inventory_item",
- fields: [
- "variants.*",
- ],
-})
-
-// inventoryItems[0].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[0].stock_locations
-```
-
-### useQueryGraphStep
-
-```ts
-import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
-
-// ...
-
-const { data: inventoryLevels } = useQueryGraphStep({
- entity: "inventory_level",
- fields: [
- "stock_locations.*",
- ],
-})
-
-// inventoryLevels[0].stock_locations
-```
-
-
# 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.
@@ -26113,6 +26312,61 @@ Consequently, the Payment Module uses the payment provider to create an account
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.
@@ -26459,61 +26713,6 @@ createRemoteLinkStep({
```
-# Payment Module Options
-
-In this document, you'll learn about the options of the Payment Module.
-
-## All Module Options
-
-|Option|Description|Required|Default|
-|---|---|---|---|---|---|---|
-|\`webhook\_delay\`|A number indicating the delay in milliseconds before processing a webhook event.|No|\`5000\`|
-|\`webhook\_retries\`|The number of times to retry the webhook event processing in case of an error.|No|\`3\`|
-|\`providers\`|An array of payment providers to install and register. Learn more |No|-|
-
-***
-
-## providers Option
-
-The `providers` option is an array of payment module providers.
-
-When the Medusa application starts, these providers are registered and can be used to process payments.
-
-For example:
-
-```ts title="medusa-config.ts"
-import { Modules } from "@medusajs/framework/utils"
-
-// ...
-
-module.exports = defineConfig({
- // ...
- modules: [
- {
- resolve: "@medusajs/medusa/payment",
- options: {
- providers: [
- {
- resolve: "@medusajs/medusa/payment-stripe",
- id: "stripe",
- options: {
- // ...
- },
- },
- ],
- },
- },
- ],
-})
-```
-
-The `providers` option is an array of objects that accept the following properties:
-
-- `resolve`: A string indicating the package name of the module provider or the path to it relative to the `src` directory.
-- `id`: A string indicating the provider's unique name or ID.
-- `options`: An optional object of the module provider's options.
-
-
# Payment
In this document, you’ll learn what a payment is and how it's created, captured, and refunded.
@@ -26561,172 +26760,41 @@ Then, the `data` property is passed to the Medusa payment provider when the paym
If you're building a custom payment provider, learn more about authorizing and capturing the payments and setting the `data` property in the [Create Payment Provider](https://docs.medusajs.com/references/payment/provider/index.html.md) guide.
-# Order Claim
+# Payment Collection
-In this document, you’ll learn about order claims.
+In this document, you’ll learn what a payment collection is and how the Medusa application uses it with the Cart Module.
-Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/claims/index.html.md) to learn how to manage an order's claims using the dashboard.
+## What's a Payment Collection?
-## What is a Claim?
+A payment collection stores payment details related to a resource, such as a cart or an order. It’s represented by the [PaymentCollection data model](https://docs.medusajs.com/references/payment/models/PaymentCollection/index.html.md).
-When a customer receives a defective or incorrect item, the merchant can create a claim to refund or replace the item.
+Every purchase or request for payment starts with a payment collection. The collection holds details necessary to complete the payment, including:
-The [OrderClaim data model](https://docs.medusajs.com/references/order/models/OrderClaim/index.html.md) represents a claim.
+- The [payment sessions](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-session/index.html.md) that represents the payment amount to authorize.
+- The [payments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment/index.html.md) that are created when a payment session is authorized. They can be captured and refunded.
+- The [payment providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/index.html.md) that handle the processing of each payment session, including the authorization, capture, and refund.
***
-## Claim Type
+## Multiple Payments
-The `Claim` data model has a `type` property whose value indicates the type of the claim:
+The payment collection supports multiple payment sessions and payments.
-- `refund`: the items are returned, and the customer is refunded.
-- `replace`: the items are returned, and the customer receives new items.
+You can use this to accept payments in increments or split payments across payment providers.
+
+
***
-## Old and Replacement Items
+## Usage with the Cart Module
-When the claim is created, a return, represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md), is also created to handle receiving the old items from the customer.
+The Cart Module provides cart management features. However, it doesn’t provide any features related to accepting payment.
-Learn more about returns in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md).
+During checkout, the Medusa application links a cart to a payment collection, which will be used for further payment processing.
-If the claim’s type is `replace`, replacement items are represented by the [ClaimItem data model](https://docs.medusajs.com/references/order/models/OrderClaimItem/index.html.md).
+It also implements the payment flow during checkout as explained in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-flow/index.html.md).
-***
-
-## Claim Shipping Methods
-
-A claim uses shipping methods to send the replacement items to the customer. These methods are represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md).
-
-The shipping methods for the returned items are associated with the claim's return, as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return#return-shipping-methods/index.html.md).
-
-***
-
-## Claim Refund
-
-If the claim’s type is `refund`, the amount to be refunded is stored in the `refund_amount` property.
-
-The [Transaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md) represents the refunds made for the claim.
-
-***
-
-## How Claims Impact an Order’s Version
-
-When a claim is confirmed, the order’s version is incremented.
-
-
-# Order Exchange
-
-In this document, you’ll learn about order exchanges.
-
-Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/exchanges/index.html.md) to learn how to manage an order's exchanges using the dashboard.
-
-## What is an Exchange?
-
-An exchange is the replacement of an item that the customer ordered with another.
-
-A merchant creates the exchange, specifying the items to be replaced and the new items to be sent.
-
-The [OrderExchange data model](https://docs.medusajs.com/references/order/models/OrderExchange/index.html.md) represents an exchange.
-
-***
-
-## Returned and New Items
-
-When the exchange is created, a return, represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md), is created to handle receiving the items back from the customer.
-
-Learn more about returns in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md).
-
-The [OrderExchangeItem data model](https://docs.medusajs.com/references/order/models/OrderExchangeItem/index.html.md) represents the new items to be sent to the customer.
-
-***
-
-## Exchange Shipping Methods
-
-An exchange has shipping methods used to send the new items to the customer. They’re represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md).
-
-The shipping methods for the returned items are associated with the exchange's return, as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return#return-shipping-methods/index.html.md).
-
-***
-
-## Exchange Payment
-
-The `Exchange` data model has a `difference_due` property that stores the outstanding amount.
-
-|Condition|Result|
-|---|---|---|
-|\`difference\_due \< 0\`|Merchant owes the customer a refund of the |
-|\`difference\_due > 0\`|Merchant requires additional payment from the customer of the |
-|\`difference\_due = 0\`|No payment processing is required.|
-
-Any payment or refund made is stored in the [Transaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md).
-
-***
-
-## How Exchanges Impact an Order’s Version
-
-When an exchange is confirmed, the order’s version is incremented.
-
-
-# Order Return
-
-In this document, you’ll learn about order returns.
-
-Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/returns/index.html.md) to learn how to manage an order's returns using the dashboard.
-
-## What is a Return?
-
-A return is the return of items delivered from the customer back to the merchant. It is represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md).
-
-A return is requested either by the customer from the storefront, or the merchant from the admin. Medusa supports an automated Return Merchandise Authorization (RMA) flow.
-
-
-
-Once the merchant receives the returned items, they mark the return as received.
-
-***
-
-## Returned Items
-
-The items to be returned are represented by the [ReturnItem data model](https://docs.medusajs.com/references/order/models/ReturnItem/index.html.md).
-
-The `ReturnItem` model has two properties storing the item's quantity:
-
-1. `received_quantity`: The quantity of the item that's received and can be added to the item's inventory quantity.
-2. `damaged_quantity`: The quantity of the item that's damaged, meaning it can't be sold again or added to the item's inventory quantity.
-
-***
-
-## Return Shipping Methods
-
-A return has shipping methods used to return the items to the merchant. The shipping methods are represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md).
-
-In the Medusa application, the shipping method for a return is created only from a shipping option, provided by the Fulfillment Module, that has the rule `is_return` enabled.
-
-***
-
-## Refund Payment
-
-The `refund_amount` property of the `Return` data model holds the amount a merchant must refund the customer.
-
-The [OrderTransaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md) represents the refunds made for the return.
-
-***
-
-## Returns in Exchanges and Claims
-
-When a merchant creates an exchange or a claim, it includes returning items from the customer.
-
-The `Return` data model also represents the return of these items. In this case, the return is associated with the exchange or claim it was created for.
-
-***
-
-## How Returns Impact an Order’s Version
-
-The order’s version is incremented when:
-
-1. A return is requested.
-2. A return is marked as received.
+
# Payment Steps in Checkout Flow
@@ -26977,86 +27045,6 @@ 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.
-
-
-
-***
-
-## data Property
-
-Payment providers may need additional data to process the payment later. For example, the ID of the session in the third-party provider.
-
-The `PaymentSession` data model has a `data` property used to store that data. It's set by the [payment provider in Medusa](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/index.html.md) when the payment is initialized.
-
-Then, when the payment session is authorized, the `data` property is used by the payment provider in Medusa to process the payment with the third-party provider.
-
-If you're building a custom payment provider, learn more about initializing the payment session and setting the `data` property in the [Create Payment Provider](https://docs.medusajs.com/references/payment/provider/index.html.md) guide.
-
-### data Property in the Storefront
-
-This `data` property is accessible in the storefront as well. So, only store in it data that can be publicly shared, and data that is useful in the storefront.
-
-For example, you can also store the client token used to initialize the payment session in the storefront with the third-party provider.
-
-***
-
-## Payment Session Status
-
-The `status` property of a payment session indicates its current status. Its value can be:
-
-- `pending`: The payment session is awaiting authorization.
-- `requires_more`: The payment session requires an action before it’s authorized. For example, to enter a 3DS code.
-- `authorized`: The payment session is authorized.
-- `error`: An error occurred while authorizing the payment.
-- `canceled`: The authorization of the payment session has been canceled.
-
-
-# Payment Collection
-
-In this document, you’ll learn what a payment collection is and how the Medusa application uses it with the Cart Module.
-
-## What's a Payment Collection?
-
-A payment collection stores payment details related to a resource, such as a cart or an order. It’s represented by the [PaymentCollection data model](https://docs.medusajs.com/references/payment/models/PaymentCollection/index.html.md).
-
-Every purchase or request for payment starts with a payment collection. The collection holds details necessary to complete the payment, including:
-
-- The [payment sessions](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-session/index.html.md) that represents the payment amount to authorize.
-- The [payments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment/index.html.md) that are created when a payment session is authorized. They can be captured and refunded.
-- The [payment providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/index.html.md) that handle the processing of each payment session, including the authorization, capture, and refund.
-
-***
-
-## Multiple Payments
-
-The payment collection supports multiple payment sessions and payments.
-
-You can use this to accept payments in increments or split payments across payment providers.
-
-
-
-***
-
-## Usage with the Cart Module
-
-The Cart Module provides cart management features. However, it doesn’t provide any features related to accepting payment.
-
-During checkout, the Medusa application links a cart to a payment collection, which will be used for further payment processing.
-
-It also implements the payment flow during checkout as explained in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-flow/index.html.md).
-
-
-
-
# Payment Module Provider
In this guide, you’ll learn about the Payment Module Provider and how it's used.
@@ -27137,6 +27125,49 @@ If you remove a payment provider from the `providers` option, the Medusa applica
Instead, the Medusa application will set the `is_enabled` property of the `PaymentProvider`'s record to `false`. This allows you to re-enable the payment provider later if needed by adding it back to the `providers` option.
+# Payment Session
+
+In this document, you’ll learn what a payment session is.
+
+## What's a Payment Session?
+
+A payment session, represented by the [PaymentSession data model](https://docs.medusajs.com/references/payment/models/PaymentSession/index.html.md), is a payment amount to be authorized. It’s associated with a payment provider that handles authorizing it.
+
+A payment collection can have multiple payment sessions. Using this feature, you can implement payment in installments or payments using multiple providers.
+
+
+
+***
+
+## data Property
+
+Payment providers may need additional data to process the payment later. For example, the ID of the session in the third-party provider.
+
+The `PaymentSession` data model has a `data` property used to store that data. It's set by the [payment provider in Medusa](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/index.html.md) when the payment is initialized.
+
+Then, when the payment session is authorized, the `data` property is used by the payment provider in Medusa to process the payment with the third-party provider.
+
+If you're building a custom payment provider, learn more about initializing the payment session and setting the `data` property in the [Create Payment Provider](https://docs.medusajs.com/references/payment/provider/index.html.md) guide.
+
+### data Property in the Storefront
+
+This `data` property is accessible in the storefront as well. So, only store in it data that can be publicly shared, and data that is useful in the storefront.
+
+For example, you can also store the client token used to initialize the payment session in the storefront with the third-party provider.
+
+***
+
+## Payment Session Status
+
+The `status` property of a payment session indicates its current status. Its value can be:
+
+- `pending`: The payment session is awaiting authorization.
+- `requires_more`: The payment session requires an action before it’s authorized. For example, to enter a 3DS code.
+- `authorized`: The payment session is authorized.
+- `error`: An error occurred while authorizing the payment.
+- `canceled`: The authorization of the payment session has been canceled.
+
+
# Payment Webhook Events
In this guide, you’ll learn how you can handle payment webhook events in your Medusa application and using the Payment Module.
@@ -27208,6 +27239,190 @@ A price list has optional `start_date` and `end_date` properties that indicate t
Its associated prices are represented by the `Price` data model.
+# Links between Pricing Module and Other Modules
+
+This document showcases the module links defined between the Pricing Module and other Commerce Modules.
+
+## Summary
+
+The Pricing Module has the following links to other modules:
+
+|First Data Model|Second Data Model|Type|Description|
+|---|---|---|---|
+|ShippingOption|PriceSet|Stored - one-to-one|Learn more|
+|ProductVariant|PriceSet|Stored - one-to-one|Learn more|
+
+***
+
+## Fulfillment Module
+
+The Fulfillment Module provides fulfillment-related functionalities, including shipping options that the customer chooses from when they place their order. However, it doesn't provide pricing-related functionalities for these options.
+
+Medusa defines a link between the `PriceSet` and `ShippingOption` data models. A shipping option's price is stored as a price set.
+
+
+
+### Retrieve with Query
+
+To retrieve the shipping option of a price set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `shipping_option.*` in `fields`:
+
+### query.graph
+
+```ts
+const { data: priceSets } = await query.graph({
+ entity: "price_set",
+ fields: [
+ "shipping_option.*",
+ ],
+})
+
+// priceSets[0].shipping_option
+```
+
+### useQueryGraphStep
+
+```ts
+import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
+
+// ...
+
+const { data: priceSets } = useQueryGraphStep({
+ entity: "price_set",
+ fields: [
+ "shipping_option.*",
+ ],
+})
+
+// priceSets[0].shipping_option
+```
+
+### Manage with Link
+
+To manage the price set of a shipping option, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):
+
+### link.create
+
+```ts
+import { Modules } from "@medusajs/framework/utils"
+
+// ...
+
+await link.create({
+ [Modules.FULFILLMENT]: {
+ shipping_option_id: "so_123",
+ },
+ [Modules.PRICING]: {
+ price_set_id: "pset_123",
+ },
+})
+```
+
+### createRemoteLinkStep
+
+```ts
+import { Modules } from "@medusajs/framework/utils"
+import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"
+
+// ...
+
+createRemoteLinkStep({
+ [Modules.FULFILLMENT]: {
+ shipping_option_id: "so_123",
+ },
+ [Modules.PRICING]: {
+ price_set_id: "pset_123",
+ },
+})
+```
+
+***
+
+## Product Module
+
+The Product Module doesn't store or manage the prices of product variants.
+
+Medusa defines a link between the `ProductVariant` and the `PriceSet`. A product variant’s prices are stored as prices belonging to a price set.
+
+
+
+So, when you want to add prices for a product variant, you create a price set and add the prices to it.
+
+You can then benefit from adding rules to prices or using the `calculatePrices` method to retrieve the price of a product variant within a specified context.
+
+### Retrieve with Query
+
+To retrieve the variant of a price set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variant.*` in `fields`:
+
+### query.graph
+
+```ts
+const { data: priceSets } = await query.graph({
+ entity: "price_set",
+ fields: [
+ "variant.*",
+ ],
+})
+
+// priceSets[0].variant
+```
+
+### useQueryGraphStep
+
+```ts
+import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
+
+// ...
+
+const { data: priceSets } = useQueryGraphStep({
+ entity: "price_set",
+ fields: [
+ "variant.*",
+ ],
+})
+
+// priceSets[0].variant
+```
+
+### Manage with Link
+
+To manage the price set of a variant, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):
+
+### link.create
+
+```ts
+import { Modules } from "@medusajs/framework/utils"
+
+// ...
+
+await link.create({
+ [Modules.PRODUCT]: {
+ variant_id: "variant_123",
+ },
+ [Modules.PRICING]: {
+ price_set_id: "pset_123",
+ },
+})
+```
+
+### createRemoteLinkStep
+
+```ts
+import { Modules } from "@medusajs/framework/utils"
+import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"
+
+// ...
+
+createRemoteLinkStep({
+ [Modules.PRODUCT]: {
+ variant_id: "variant_123",
+ },
+ [Modules.PRICING]: {
+ price_set_id: "pset_123",
+ },
+})
+```
+
+
# Prices Calculation
In this document, you'll learn how prices are calculated when you use the [calculatePrices method](https://docs.medusajs.com/references/pricing/calculatePrices/index.html.md) of the Pricing Module's main service.
@@ -27434,190 +27649,6 @@ const price = await pricingModuleService.calculatePrices(
### Result
-# Links between Pricing Module and Other Modules
-
-This document showcases the module links defined between the Pricing Module and other Commerce Modules.
-
-## Summary
-
-The Pricing Module has the following links to other modules:
-
-|First Data Model|Second Data Model|Type|Description|
-|---|---|---|---|
-|ShippingOption|PriceSet|Stored - one-to-one|Learn more|
-|ProductVariant|PriceSet|Stored - one-to-one|Learn more|
-
-***
-
-## Fulfillment Module
-
-The Fulfillment Module provides fulfillment-related functionalities, including shipping options that the customer chooses from when they place their order. However, it doesn't provide pricing-related functionalities for these options.
-
-Medusa defines a link between the `PriceSet` and `ShippingOption` data models. A shipping option's price is stored as a price set.
-
-
-
-### Retrieve with Query
-
-To retrieve the shipping option of a price set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `shipping_option.*` in `fields`:
-
-### query.graph
-
-```ts
-const { data: priceSets } = await query.graph({
- entity: "price_set",
- fields: [
- "shipping_option.*",
- ],
-})
-
-// priceSets[0].shipping_option
-```
-
-### useQueryGraphStep
-
-```ts
-import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
-
-// ...
-
-const { data: priceSets } = useQueryGraphStep({
- entity: "price_set",
- fields: [
- "shipping_option.*",
- ],
-})
-
-// priceSets[0].shipping_option
-```
-
-### Manage with Link
-
-To manage the price set of a shipping option, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):
-
-### link.create
-
-```ts
-import { Modules } from "@medusajs/framework/utils"
-
-// ...
-
-await link.create({
- [Modules.FULFILLMENT]: {
- shipping_option_id: "so_123",
- },
- [Modules.PRICING]: {
- price_set_id: "pset_123",
- },
-})
-```
-
-### createRemoteLinkStep
-
-```ts
-import { Modules } from "@medusajs/framework/utils"
-import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"
-
-// ...
-
-createRemoteLinkStep({
- [Modules.FULFILLMENT]: {
- shipping_option_id: "so_123",
- },
- [Modules.PRICING]: {
- price_set_id: "pset_123",
- },
-})
-```
-
-***
-
-## Product Module
-
-The Product Module doesn't store or manage the prices of product variants.
-
-Medusa defines a link between the `ProductVariant` and the `PriceSet`. A product variant’s prices are stored as prices belonging to a price set.
-
-
-
-So, when you want to add prices for a product variant, you create a price set and add the prices to it.
-
-You can then benefit from adding rules to prices or using the `calculatePrices` method to retrieve the price of a product variant within a specified context.
-
-### Retrieve with Query
-
-To retrieve the variant of a price set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variant.*` in `fields`:
-
-### query.graph
-
-```ts
-const { data: priceSets } = await query.graph({
- entity: "price_set",
- fields: [
- "variant.*",
- ],
-})
-
-// priceSets[0].variant
-```
-
-### useQueryGraphStep
-
-```ts
-import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
-
-// ...
-
-const { data: priceSets } = useQueryGraphStep({
- entity: "price_set",
- fields: [
- "variant.*",
- ],
-})
-
-// priceSets[0].variant
-```
-
-### Manage with Link
-
-To manage the price set of a variant, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):
-
-### link.create
-
-```ts
-import { Modules } from "@medusajs/framework/utils"
-
-// ...
-
-await link.create({
- [Modules.PRODUCT]: {
- variant_id: "variant_123",
- },
- [Modules.PRICING]: {
- price_set_id: "pset_123",
- },
-})
-```
-
-### createRemoteLinkStep
-
-```ts
-import { Modules } from "@medusajs/framework/utils"
-import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"
-
-// ...
-
-createRemoteLinkStep({
- [Modules.PRODUCT]: {
- variant_id: "variant_123",
- },
- [Modules.PRICING]: {
- price_set_id: "pset_123",
- },
-})
-```
-
-
# Price Tiers and Rules
In this Pricing Module guide, you'll learn about tired prices, price rules for price sets and price lists, and how to add rules to a price.
@@ -28333,125 +28364,6 @@ createRemoteLinkStep({
```
-# Configure Selling Products
-
-In this guide, you'll learn how to set up and configure your products based on their shipping and inventory requirements, the product type, how you want to sell them, or your commerce ecosystem.
-
-The concepts in this guide are applicable starting from Medusa v2.5.1.
-
-## Scenario
-
-Businesses can have different selling requirements:
-
-1. They may sell physical or digital items.
-2. They may sell items that don't require shipping or inventory management, such as selling digital products, services, or booking appointments.
-3. They may sell items whose inventory is managed by an external system, such as an ERP.
-
-Medusa supports these different selling requirements by allowing you to configure shipping and inventory requirements for products and their variants. This guide explains how these configurations work, then provides examples of setting up different use cases.
-
-***
-
-## Configuring Shipping Requirements
-
-The Medusa application defines a link between the `Product` data model and a [ShippingProfile](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/concepts#shipping-profile/index.html.md) in the [Fulfillment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/index.html.md), allowing you to associate a product with a shipping profile.
-
-When a product is associated with a shipping profile, its variants require shipping and fulfillment when purchased. This is useful for physical products or digital products that require custom fulfillment.
-
-If a product doesn't have an associated shipping profile, its variants don't require shipping and fulfillment when purchased. This is useful for digital products, for example, that don't require shipping.
-
-### Overriding Shipping Requirements for Variants
-
-A product variant whose inventory is managed by Medusa (its `manage_inventory` property is enabled) has an [inventory item](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts#inventoryitem/index.html.md). The inventory item has a `requires_shipping` property that can be used to override its shipping requirement. This is useful if the product has an associated shipping profile but you want to disable shipping for a specific variant, or vice versa.
-
-Learn more about product variant's inventory in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/variant-inventory/index.html.md).
-
-When a product variant is purchased, the Medusa application decides whether the purchased item requires shipping in the following order:
-
-1. The product variant has an inventory item. In this case, the Medusa application uses the inventory item's `requires_shipping` property to determine if the item requires shipping.
-2. If the product variant doesn't have an inventory item, the Medusa application checks whether the product has an associated shipping profile to determine if the item requires shipping.
-
-***
-
-## Use Case Examples
-
-By combining configurations of shipment requirements and inventory management, you can set up your products to support your use case:
-
-|Use Case|Configurations|Example|
-|---|---|---|---|---|
-|Item that's shipped on purchase, and its variant inventory is managed by the Medusa application.||Any stock-kept item (clothing, for example), whose inventory is managed in the Medusa application.|
-|Item that's shipped on purchase, but its variant inventory is managed externally (not by Medusa) or it has infinite stock.||Any stock-kept item (clothing, for example), whose inventory is managed in an ERP or has infinite stock.|
-|Item that's not shipped on purchase, but its variant inventory is managed by Medusa.||Digital products, such as licenses, that don't require shipping but have a limited quantity.|
-|Item that doesn't require shipping and its variant inventory isn't managed by Medusa.|||
-
-
-# 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
-
-
# Product Variant Inventory
# Product Variant Inventory
@@ -28519,523 +28431,55 @@ 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).
-# Promotion Actions
+# Configure Selling Products
-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).
+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.
-## computeActions Method
+The concepts in this guide are applicable starting from Medusa v2.5.1.
-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.
+## Scenario
-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.
+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.
***
-## Action Types
+## Configuring Shipping Requirements
-### `addItemAdjustment` Action
+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.
-The `addItemAdjustment` action indicates that an adjustment must be made to an item. For example, removing $5 off its amount.
+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.
-This action has the following format:
+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.
-```ts
-export interface AddItemAdjustmentAction {
- action: "addItemAdjustment"
- item_id: string
- amount: number
- code: string
- description?: string
-}
-```
+### Overriding Shipping Requirements for Variants
-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.
+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.
-Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.AddItemAdjustmentAction/index.html.md) for details on the object’s properties.
+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).
-### `removeItemAdjustment` Action
+When a product variant is purchased, the Medusa application decides whether the purchased item requires shipping in the following order:
-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.
-
-
-# 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.
-
-
-
-In this example, the promotion is only applied on products in the cart having the SKU `SHIRT`.
+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.
***
-## Buy Promotion Rules
+## Use Case Examples
-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.
+By combining configurations of shipment requirements and inventory management, you can set up your products to support your use case:
-The application method has a collection of `PromotionRule` items to define the “buy X” rule. The `buy_rules` property represents this relation.
-
-
-
-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.
-
-
-
-***
-
-## 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.
-
-
-
-
-# Promotion Concepts
-
-In this guide, 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.
-
-***
-
-## Promotion Rules
-
-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 `PromotionRule`'s `attribute` property indicates the property's name to which this rule is applied. For example, `customer_group_id`.
-
-The expected value for the attribute 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`.
-
-
-
-In this case, a customer’s group must be in the `VIP` and `B2B` set of values to use the promotion.
-
-***
-
-## How to Apply Rules on a Promotion?
-
-### Using Workflows
-
-If you're managing promotions using [Medusa's workflows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/medusa-workflows-reference/index.html.md) or the API routes that use them, you can specify rules for the promotion or its [application method](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/application-method/index.html.md).
-
-For example, if you're creating a promotion using the [createPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionsWorkflow/index.html.md):
-
-```ts
-const { result } = await createPromotionsWorkflow(container)
- .run({
- input: {
- promotionsData: [{
- code: "10OFF",
- type: "standard",
- status: "active",
- application_method: {
- type: "percentage",
- target_type: "items",
- allocation: "across",
- value: 10,
- currency_code: "usd",
- },
- rules: [
- {
- attribute: "customer.group.id",
- operator: "eq",
- values: [
- "cusgrp_123",
- ],
- },
- ],
- }],
- },
- })
-```
-
-In this example, the promotion is restricted to customers with the `cusgrp_123` customer group.
-
-### Using Promotion Module's Service
-
-For most use cases, it's recommended to use [workflows](#using-workflows) instead of directly using the module's service.
-
-If you're managing promotions using the Promotion Module's service, you can specify rules for the promotion or its [application method](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/application-method/index.html.md) in its methods.
-
-For example, if you're creating a promotion with the [createPromotions](https://docs.medusajs.com/references/promotion/createPromotions/index.html.md) method:
-
-```ts
-const promotions = await promotionModuleService.createPromotions([
- {
- code: "50OFF",
- type: "standard",
- status: "active",
- application_method: {
- type: "percentage",
- target_type: "items",
- value: 50,
- },
- rules: [
- {
- attribute: "customer.group.id",
- operator: "eq",
- values: [
- "cusgrp_123",
- ],
- },
- ],
- },
-])
-```
-
-In this example, the promotion is restricted to customers with the `cusgrp_123` customer group.
-
-### How is the Promotion Rule Applied?
-
-A promotion is applied on a resource if its attributes match the promotion's rules.
-
-For example, consider you have the following promotion with a rule that restricts the promotion to a specific customer:
-
-```json
-{
- "code": "10OFF",
- "type": "standard",
- "status": "active",
- "application_method": {
- "type": "percentage",
- "target_type": "items",
- "allocation": "across",
- "value": 10,
- "currency_code": "usd"
- },
- "rules": [
- {
- "attribute": "customer_id",
- "operator": "eq",
- "values": [
- "cus_123"
- ]
- }
- ]
-}
-```
-
-When you try to apply this promotion on a cart, the cart's `customer_id` is compared to the promotion rule's value based on the specified operator. So, the promotion will only be applied if the cart's `customer_id` is equal to `cus_123`.
-
-
-# Links between Promotion Module and Other Modules
-
-This document showcases the module links defined between the Promotion Module and other Commerce Modules.
-
-## Summary
-
-The Promotion Module has the following links to other modules:
-
-Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database.
-
-|First Data Model|Second Data Model|Type|Description|
-|---|---|---|---|
-|Cart|Promotion|Stored - many-to-many|Learn more|
-|LineItemAdjustment|Promotion|Read-only - has one|Learn more|
-|Order|Promotion|Stored - many-to-many|Learn more|
-
-***
-
-## Cart Module
-
-A promotion can be applied on line items and shipping methods of a cart. Medusa defines a link between the `Cart` and `Promotion` data models.
-
-
-
-Medusa also defines a read-only link between the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `LineItemAdjustment` data model and the `Promotion` data model. Because the link is read-only from the `LineItemAdjustment`'s side, you can only retrieve the promotion applied on a line item, and not the other way around.
-
-### Retrieve with Query
-
-To retrieve the carts that a promotion is applied on with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `carts.*` in `fields`:
-
-To retrieve the promotion of a line item adjustment, pass `promotion.*` in `fields`.
-
-### query.graph
-
-```ts
-const { data: promotions } = await query.graph({
- entity: "promotion",
- fields: [
- "carts.*",
- ],
-})
-
-// promotions[0].carts
-```
-
-### useQueryGraphStep
-
-```ts
-import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
-
-// ...
-
-const { data: promotions } = useQueryGraphStep({
- entity: "promotion",
- fields: [
- "carts.*",
- ],
-})
-
-// promotions[0].carts
-```
-
-### Manage with Link
-
-To manage the promotions of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):
-
-### link.create
-
-```ts
-import { Modules } from "@medusajs/framework/utils"
-
-// ...
-
-await link.create({
- [Modules.CART]: {
- cart_id: "cart_123",
- },
- [Modules.PROMOTION]: {
- promotion_id: "promo_123",
- },
-})
-```
-
-### createRemoteLinkStep
-
-```ts
-import { Modules } from "@medusajs/framework/utils"
-import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"
-
-// ...
-
-createRemoteLinkStep({
- [Modules.CART]: {
- cart_id: "cart_123",
- },
- [Modules.PROMOTION]: {
- promotion_id: "promo_123",
- },
-})
-```
-
-***
-
-## Order Module
-
-An order is associated with the promotion applied on it. Medusa defines a link between the `Order` and `Promotion` data models.
-
-
-
-### Retrieve with Query
-
-To retrieve the orders a promotion is applied on with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `orders.*` in `fields`:
-
-### query.graph
-
-```ts
-const { data: promotions } = await query.graph({
- entity: "promotion",
- fields: [
- "orders.*",
- ],
-})
-
-// promotions[0].orders
-```
-
-### useQueryGraphStep
-
-```ts
-import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
-
-// ...
-
-const { data: promotions } = useQueryGraphStep({
- entity: "promotion",
- fields: [
- "orders.*",
- ],
-})
-
-// promotions[0].orders
-```
-
-### Manage with Link
-
-To manage the promotion of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):
-
-### link.create
-
-```ts
-import { Modules } from "@medusajs/framework/utils"
-
-// ...
-
-await link.create({
- [Modules.ORDER]: {
- order_id: "order_123",
- },
- [Modules.PROMOTION]: {
- promotion_id: "promo_123",
- },
-})
-```
-
-### createRemoteLinkStep
-
-```ts
-import { Modules } from "@medusajs/framework/utils"
-import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"
-
-// ...
-
-createRemoteLinkStep({
- [Modules.ORDER]: {
- order_id: "order_123",
- },
- [Modules.PROMOTION]: {
- promotion_id: "promo_123",
- },
-})
-```
+|Use Case|Configurations|Example|
+|---|---|---|---|---|
+|Item that's shipped on purchase, and its variant inventory is managed by the Medusa application.||Any stock-kept item (clothing, for example), whose inventory is managed in the Medusa application.|
+|Item that's shipped on purchase, but its variant inventory is managed externally (not by Medusa) or it has infinite stock.||Any stock-kept item (clothing, for example), whose inventory is managed in an ERP or has infinite stock.|
+|Item that's not shipped on purchase, but its variant inventory is managed by Medusa.||Digital products, such as licenses, that don't require shipping but have a limited quantity.|
+|Item that doesn't require shipping and its variant inventory isn't managed by Medusa.|||
# Links between Region Module and Other Modules
@@ -29621,6 +29065,593 @@ You can then use these IDs based on your business logic. For example, you can re
Notice that the request object's type is `MedusaStoreRequest` instead of `MedusaRequest` to ensure the availability of the `publishable_key_context` property.
+# 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 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.
+
+
+# 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.
+
+
+
+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.
+
+
+
+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.
+
+
+
+***
+
+## 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.
+
+
+
+
+# Promotion Concepts
+
+In this guide, 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.
+
+***
+
+## Promotion Rules
+
+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 `PromotionRule`'s `attribute` property indicates the property's name to which this rule is applied. For example, `customer_group_id`.
+
+The expected value for the attribute 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`.
+
+
+
+In this case, a customer’s group must be in the `VIP` and `B2B` set of values to use the promotion.
+
+***
+
+## How to Apply Rules on a Promotion?
+
+### Using Workflows
+
+If you're managing promotions using [Medusa's workflows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/medusa-workflows-reference/index.html.md) or the API routes that use them, you can specify rules for the promotion or its [application method](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/application-method/index.html.md).
+
+For example, if you're creating a promotion using the [createPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionsWorkflow/index.html.md):
+
+```ts
+const { result } = await createPromotionsWorkflow(container)
+ .run({
+ input: {
+ promotionsData: [{
+ code: "10OFF",
+ type: "standard",
+ status: "active",
+ application_method: {
+ type: "percentage",
+ target_type: "items",
+ allocation: "across",
+ value: 10,
+ currency_code: "usd",
+ },
+ rules: [
+ {
+ attribute: "customer.group.id",
+ operator: "eq",
+ values: [
+ "cusgrp_123",
+ ],
+ },
+ ],
+ }],
+ },
+ })
+```
+
+In this example, the promotion is restricted to customers with the `cusgrp_123` customer group.
+
+### Using Promotion Module's Service
+
+For most use cases, it's recommended to use [workflows](#using-workflows) instead of directly using the module's service.
+
+If you're managing promotions using the Promotion Module's service, you can specify rules for the promotion or its [application method](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/application-method/index.html.md) in its methods.
+
+For example, if you're creating a promotion with the [createPromotions](https://docs.medusajs.com/references/promotion/createPromotions/index.html.md) method:
+
+```ts
+const promotions = await promotionModuleService.createPromotions([
+ {
+ code: "50OFF",
+ type: "standard",
+ status: "active",
+ application_method: {
+ type: "percentage",
+ target_type: "items",
+ value: 50,
+ },
+ rules: [
+ {
+ attribute: "customer.group.id",
+ operator: "eq",
+ values: [
+ "cusgrp_123",
+ ],
+ },
+ ],
+ },
+])
+```
+
+In this example, the promotion is restricted to customers with the `cusgrp_123` customer group.
+
+### How is the Promotion Rule Applied?
+
+A promotion is applied on a resource if its attributes match the promotion's rules.
+
+For example, consider you have the following promotion with a rule that restricts the promotion to a specific customer:
+
+```json
+{
+ "code": "10OFF",
+ "type": "standard",
+ "status": "active",
+ "application_method": {
+ "type": "percentage",
+ "target_type": "items",
+ "allocation": "across",
+ "value": 10,
+ "currency_code": "usd"
+ },
+ "rules": [
+ {
+ "attribute": "customer_id",
+ "operator": "eq",
+ "values": [
+ "cus_123"
+ ]
+ }
+ ]
+}
+```
+
+When you try to apply this promotion on a cart, the cart's `customer_id` is compared to the promotion rule's value based on the specified operator. So, the promotion will only be applied if the cart's `customer_id` is equal to `cus_123`.
+
+
+# Links between Promotion Module and Other Modules
+
+This document showcases the module links defined between the Promotion Module and other Commerce Modules.
+
+## Summary
+
+The Promotion Module has the following links to other modules:
+
+Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database.
+
+|First Data Model|Second Data Model|Type|Description|
+|---|---|---|---|
+|Cart|Promotion|Stored - many-to-many|Learn more|
+|LineItemAdjustment|Promotion|Read-only - has one|Learn more|
+|Order|Promotion|Stored - many-to-many|Learn more|
+
+***
+
+## Cart Module
+
+A promotion can be applied on line items and shipping methods of a cart. Medusa defines a link between the `Cart` and `Promotion` data models.
+
+
+
+Medusa also defines a read-only link between the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `LineItemAdjustment` data model and the `Promotion` data model. Because the link is read-only from the `LineItemAdjustment`'s side, you can only retrieve the promotion applied on a line item, and not the other way around.
+
+### Retrieve with Query
+
+To retrieve the carts that a promotion is applied on with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `carts.*` in `fields`:
+
+To retrieve the promotion of a line item adjustment, pass `promotion.*` in `fields`.
+
+### query.graph
+
+```ts
+const { data: promotions } = await query.graph({
+ entity: "promotion",
+ fields: [
+ "carts.*",
+ ],
+})
+
+// promotions[0].carts
+```
+
+### useQueryGraphStep
+
+```ts
+import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
+
+// ...
+
+const { data: promotions } = useQueryGraphStep({
+ entity: "promotion",
+ fields: [
+ "carts.*",
+ ],
+})
+
+// promotions[0].carts
+```
+
+### Manage with Link
+
+To manage the promotions of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):
+
+### link.create
+
+```ts
+import { Modules } from "@medusajs/framework/utils"
+
+// ...
+
+await link.create({
+ [Modules.CART]: {
+ cart_id: "cart_123",
+ },
+ [Modules.PROMOTION]: {
+ promotion_id: "promo_123",
+ },
+})
+```
+
+### createRemoteLinkStep
+
+```ts
+import { Modules } from "@medusajs/framework/utils"
+import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"
+
+// ...
+
+createRemoteLinkStep({
+ [Modules.CART]: {
+ cart_id: "cart_123",
+ },
+ [Modules.PROMOTION]: {
+ promotion_id: "promo_123",
+ },
+})
+```
+
+***
+
+## Order Module
+
+An order is associated with the promotion applied on it. Medusa defines a link between the `Order` and `Promotion` data models.
+
+
+
+### Retrieve with Query
+
+To retrieve the orders a promotion is applied on with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `orders.*` in `fields`:
+
+### query.graph
+
+```ts
+const { data: promotions } = await query.graph({
+ entity: "promotion",
+ fields: [
+ "orders.*",
+ ],
+})
+
+// promotions[0].orders
+```
+
+### useQueryGraphStep
+
+```ts
+import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
+
+// ...
+
+const { data: promotions } = useQueryGraphStep({
+ entity: "promotion",
+ fields: [
+ "orders.*",
+ ],
+})
+
+// promotions[0].orders
+```
+
+### Manage with Link
+
+To manage the promotion of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):
+
+### link.create
+
+```ts
+import { Modules } from "@medusajs/framework/utils"
+
+// ...
+
+await link.create({
+ [Modules.ORDER]: {
+ order_id: "order_123",
+ },
+ [Modules.PROMOTION]: {
+ promotion_id: "promo_123",
+ },
+})
+```
+
+### createRemoteLinkStep
+
+```ts
+import { Modules } from "@medusajs/framework/utils"
+import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"
+
+// ...
+
+createRemoteLinkStep({
+ [Modules.ORDER]: {
+ order_id: "order_123",
+ },
+ [Modules.PROMOTION]: {
+ promotion_id: "promo_123",
+ },
+})
+```
+
+
# Stock Location Concepts
In this document, you’ll learn about the main concepts in the Stock Location Module.
@@ -29868,63 +29899,6 @@ createRemoteLinkStep({
```
-# Links between Store Module and Other Modules
-
-This document showcases the module links defined between the Store Module and other Commerce Modules.
-
-## Summary
-
-The Store Module has the following links to other modules:
-
-Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database.
-
-|First Data Model|Second Data Model|Type|Description|
-|---|---|---|---|
-|StoreCurrency|Currency|Read-only - has many|Learn more|
-
-***
-
-## Currency Module
-
-The Store Module has a `Currency` data model that stores the supported currencies of a store. However, these currencies don't hold all the details of a currency, such as its name or symbol.
-
-Instead, Medusa defines a read-only link between the [Currency Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/currency/index.html.md)'s `Currency` data model and the Store Module's `StoreCurrency` data model. This means you can retrieve the details of a store's supported currencies, but you don't manage the links in a pivot table in the database. The currencies of a store are determined by the `currency_code` of the [Currency](https://docs.medusajs.com/references/store/models/StoreCurrency/index.html.md) data model in the Store Module (not in the Currency Module).
-
-### Retrieve with Query
-
-To retrieve the details of a store's currencies with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `supported_currencies.currency.*` in `fields`:
-
-### query.graph
-
-```ts
-const { data: stores } = await query.graph({
- entity: "store",
- fields: [
- "supported_currencies.currency.*",
- ],
-})
-
-// stores[0].supported_currencies
-```
-
-### useQueryGraphStep
-
-```ts
-import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
-
-// ...
-
-const { data: stores } = useQueryGraphStep({
- entity: "store",
- fields: [
- "supported_currencies.currency.*",
- ],
-})
-
-// stores[0].supported_currencies
-```
-
-
# Tax Module Options
In this guide, you'll learn about the options of the Tax Module.
@@ -30037,6 +30011,80 @@ A tax module implements the logic to shape tax lines. Each tax region uses a tax
Learn more about tax providers, configuring, and creating them in the [Tax Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-provider/index.html.md) guide.
+# Tax Module Provider
+
+In this guide, you’ll learn about the Tax Module Provider and how it's used.
+
+Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md) to learn how to manage tax provider of a tax region using the dashboard.
+
+## What is a Tax Module Provider?
+
+The Tax Module Provider handles tax line calculations in the Medusa application. It integrates third-party tax services, such as TaxJar, or implements custom tax calculation logic.
+
+The Medusa application uses the Tax Module Provider whenever it needs to calculate tax lines for a cart or order, or when you [calculate the tax lines using the Tax Module's service](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-calculation-with-provider/index.html.md).
+
+
+
+***
+
+## Default Tax Provider
+
+The Tax Module provides a `system` tax provider that acts as a placeholder tax provider. It performs basic tax calculation, as you can see in the [Create Tax Module Provider](https://docs.medusajs.com/references/tax/provider#gettaxlines/index.html.md) guide.
+
+This provider is installed by default in your application and you can use it with tax regions.
+
+The identifier of the system tax provider is `tp_system`.
+
+***
+
+## How to Create a Custom Tax Provider?
+
+A Tax Module Provider is a module whose service implements the `ITaxProvider` imported from `@medusajs/framework/types`.
+
+The module can have multiple tax provider services, where each are registered as separate tax providers.
+
+Refer to the [Create Tax Module Provider](https://docs.medusajs.com/references/tax/provider/index.html.md) guide to learn how to create a Tax Module Provider.
+
+After you create a tax provider, you can choose it as the default Tax Module Provider for a region in the [Medusa Admin dashboard](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md).
+
+***
+
+## How are Tax Providers Registered?
+
+### Configure Tax Module's Providers
+
+The Tax Module accepts a `providers` option that allows you to configure the providers registered in your application.
+
+Learn more about this option in the [Module Options](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/module-options/index.html.md) guide.
+
+### Registration on Application Start
+
+When the Medusa application starts, it registers the Tax Module Providers defined in the `providers` option of the Tax Module.
+
+For each Tax Module Provider, the Medusa application finds all tax provider services defined in them to register.
+
+### TaxProvider Data Model
+
+A registered tax provider is represented by the [TaxProvider data model](https://docs.medusajs.com/references/tax/models/TaxProvider/index.html.md) in the Medusa application.
+
+This data model is used to reference a service in the Tax Module Provider and determine whether it's installed in the application.
+
+
+
+The `TaxProvider` data model has the following properties:
+
+- `id`: The unique identifier of the tax provider. The ID's format is `tp_{identifier}_{id}`, where:
+ - `identifier` is the value of the `identifier` property in the Tax Module Provider's service.
+ - `id` is the value of the `id` property of the Tax Module Provider in `medusa-config.ts`.
+- `is_enabled`: A boolean indicating whether the tax provider is enabled.
+
+### How to Remove a Tax Provider?
+
+You can remove a registered tax provider from the Medusa application by removing it from the `providers` option in the Tax Module's configuration.
+
+Then, the next time the Medusa application starts, it will set the `is_enabled` property of the `TaxProvider`'s record to `false`. This allows you to re-enable the tax provider later if needed by adding it back to the `providers` option.
+
+
# Tax Rates and Rules
In this document, you’ll learn about tax rates and rules.
@@ -30110,78 +30158,61 @@ You can use Medusa's default tax provider or create a custom one, allowing you t
Learn more about tax providers in the [Tax Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-provider/index.html.md) guide.
-# Tax Module Provider
+# Links between Store Module and Other Modules
-In this guide, you’ll learn about the Tax Module Provider and how it's used.
+This document showcases the module links defined between the Store Module and other Commerce Modules.
-Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md) to learn how to manage tax provider of a tax region using the dashboard.
+## Summary
-## What is a Tax Module Provider?
+The Store Module has the following links to other modules:
-The Tax Module Provider handles tax line calculations in the Medusa application. It integrates third-party tax services, such as TaxJar, or implements custom tax calculation logic.
+Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database.
-The Medusa application uses the Tax Module Provider whenever it needs to calculate tax lines for a cart or order, or when you [calculate the tax lines using the Tax Module's service](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-calculation-with-provider/index.html.md).
-
-
+|First Data Model|Second Data Model|Type|Description|
+|---|---|---|---|
+|StoreCurrency|Currency|Read-only - has many|Learn more|
***
-## Default Tax Provider
+## Currency Module
-The Tax Module provides a `system` tax provider that acts as a placeholder tax provider. It performs basic tax calculation, as you can see in the [Create Tax Module Provider](https://docs.medusajs.com/references/tax/provider#gettaxlines/index.html.md) guide.
+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.
-This provider is installed by default in your application and you can use it with tax regions.
+Instead, Medusa defines a read-only link between the [Currency Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/currency/index.html.md)'s `Currency` data model and the Store Module's `StoreCurrency` data model. This means you can retrieve the details of a store's supported currencies, but you don't manage the links in a pivot table in the database. The currencies of a store are determined by the `currency_code` of the [Currency](https://docs.medusajs.com/references/store/models/StoreCurrency/index.html.md) data model in the Store Module (not in the Currency Module).
-The identifier of the system tax provider is `tp_system`.
+### 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`:
-## How to Create a Custom Tax Provider?
+### query.graph
-A Tax Module Provider is a module whose service implements the `ITaxProvider` imported from `@medusajs/framework/types`.
+```ts
+const { data: stores } = await query.graph({
+ entity: "store",
+ fields: [
+ "supported_currencies.currency.*",
+ ],
+})
-The module can have multiple tax provider services, where each are registered as separate tax providers.
+// stores[0].supported_currencies
+```
-Refer to the [Create Tax Module Provider](https://docs.medusajs.com/references/tax/provider/index.html.md) guide to learn how to create a Tax Module Provider.
+### useQueryGraphStep
-After you create a tax provider, you can choose it as the default Tax Module Provider for a region in the [Medusa Admin dashboard](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md).
+```ts
+import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
-***
+// ...
-## How are Tax Providers Registered?
+const { data: stores } = useQueryGraphStep({
+ entity: "store",
+ fields: [
+ "supported_currencies.currency.*",
+ ],
+})
-### Configure Tax Module's Providers
-
-The Tax Module accepts a `providers` option that allows you to configure the providers registered in your application.
-
-Learn more about this option in the [Module Options](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/module-options/index.html.md) guide.
-
-### Registration on Application Start
-
-When the Medusa application starts, it registers the Tax Module Providers defined in the `providers` option of the Tax Module.
-
-For each Tax Module Provider, the Medusa application finds all tax provider services defined in them to register.
-
-### TaxProvider Data Model
-
-A registered tax provider is represented by the [TaxProvider data model](https://docs.medusajs.com/references/tax/models/TaxProvider/index.html.md) in the Medusa application.
-
-This data model is used to reference a service in the Tax Module Provider and determine whether it's installed in the application.
-
-
-
-The `TaxProvider` data model has the following properties:
-
-- `id`: The unique identifier of the tax provider. The ID's format is `tp_{identifier}_{id}`, where:
- - `identifier` is the value of the `identifier` property in the Tax Module Provider's service.
- - `id` is the value of the `id` property of the Tax Module Provider in `medusa-config.ts`.
-- `is_enabled`: A boolean indicating whether the tax provider is enabled.
-
-### How to Remove a Tax Provider?
-
-You can remove a registered tax provider from the Medusa application by removing it from the `providers` option in the Tax Module's configuration.
-
-Then, the next time the Medusa application starts, it will set the `is_enabled` property of the `TaxProvider`'s record to `false`. This allows you to re-enable the tax provider later if needed by adding it back to the `providers` option.
+// stores[0].supported_currencies
+```
# User Module Options
@@ -30573,159 +30604,6 @@ For the context of the product variant's calculated price, you pass an object to
Each variant in the retrieved products has a `calculated_price` object. Learn more about its properties in [this Pricing Module guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#returned-price-object/index.html.md).
-# Get Product Variant Inventory Quantity
-
-In this guide, you'll learn how to retrieve the available inventory quantity of a product variant in your Medusa application customizations. That includes API routes, workflows, subscribers, scheduled jobs, and any resource that can access the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md).
-
-Refer to the [Retrieve Product Variant Inventory](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/products/inventory/index.html.md) storefront guide.
-
-## Understanding Product Variant Inventory Availability
-
-Product variants have a `manage_inventory` boolean field that indicates whether the Medusa application manages the inventory of the product variant.
-
-When `manage_inventory` is disabled, the Medusa application always considers the product variant to be in stock. So, you can't retrieve the inventory quantity for those products.
-
-When `manage_inventory` is enabled, the Medusa application tracks the inventory of the product variant using the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md). For example, when a customer purchases a product variant, the Medusa application decrements the stocked quantity of the product variant.
-
-This guide explains how to retrieve the inventory quantity of a product variant when `manage_inventory` is enabled.
-
-***
-
-## Retrieve Product Variant Inventory
-
-To retrieve the inventory quantity of a product variant, use the `getVariantAvailability` utility function imported from `@medusajs/framework/utils`. It returns the available quantity of the product variant.
-
-For example:
-
-```ts highlights={variantAvailabilityHighlights}
-import { getVariantAvailability } from "@medusajs/framework/utils"
-
-// ...
-
-// use req.scope instead of container in API routes
-const query = container.resolve("query")
-
-const availability = await getVariantAvailability(query, {
- variant_ids: ["variant_123"],
- sales_channel_id: "sc_123",
-})
-```
-
-A product variant's inventory quantity is set per [stock location](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/index.html.md). This stock location is linked to a [sales channel](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/index.html.md).
-
-So, to retrieve the inventory quantity of a product variant using `getVariantAvailability`, you need to also provide the ID of the sales channel to retrieve the inventory quantity in.
-
-Refer to the [Retrieve Sales Channel to Use](#retrieve-sales-channel-to-use) section to learn how to retrieve the sales channel ID to use in the `getVariantAvailability` function.
-
-### Parameters
-
-The `getVariantAvailability` function accepts the following parameters:
-
-- query: (Query) Instance of Query to retrieve the necessary data.
-- options: (\`object\`) The options to retrieve the variant availability.
-
- - variant\_ids: (\`string\[]\`) The IDs of the product variants to retrieve their inventory availability.
-
- - sales\_channel\_id: (\`string\`) The ID of the sales channel to retrieve the variant availability in.
-
-### Returns
-
-The `getVariantAvailability` function resolves to an object whose keys are the IDs of each product variant passed in the `variant_ids` parameter.
-
-The value of each key is an object with the following properties:
-
-- availability: (\`number\`) The available quantity of the product variant in the stock location linked to the sales channel. If \`manage\_inventory\` is disabled, this value is \`0\`.
-- sales\_channel\_id: (\`string\`) The ID of the sales channel that the availability is scoped to.
-
-For example, the object may look like this:
-
-```json title="Example result"
-{
- "variant_123": {
- "availability": 10,
- "sales_channel_id": "sc_123"
- }
-}
-```
-
-***
-
-## Retrieve Sales Channel to Use
-
-To retrieve the sales channel ID to use in the `getVariantAvailability` function, you can either:
-
-- Use the sales channel of the request's scope.
-- Use the sales channel that the variant's product is available in.
-
-### Method 1: Use Sales Channel Scope in Store Routes
-
-Requests sent to API routes starting with `/store` must include a [publishable API key in the request header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/publishable-api-keys/index.html.md). This scopes the request to one or more sales channels associated with the publishable API key.
-
-So, if you're retrieving the variant inventory availability in an API route starting with `/store`, you can access the sales channel using the `publishable_key_context.sales_channel_ids` property of the request object:
-
-```ts highlights={salesChannelScopeHighlights}
-import { MedusaStoreRequest, MedusaResponse } from "@medusajs/framework/http"
-import { getVariantAvailability } from "@medusajs/framework/utils"
-
-export async function GET(
- req: MedusaStoreRequest,
- res: MedusaResponse
-) {
- const query = req.scope.resolve("query")
- const sales_channel_ids = req.publishable_key_context.sales_channel_ids
-
- const availability = await getVariantAvailability(query, {
- variant_ids: ["variant_123"],
- sales_channel_id: sales_channel_ids[0],
- })
-
- res.json({
- availability,
- })
-}
-```
-
-In this example, you retrieve the scope's sales channel IDs using `req.publishable_key_context.sales_channel_ids`, whose value is an array of IDs.
-
-Then, you pass the first sales channel ID to the `getVariantAvailability` function to retrieve the inventory availability of the product variant in that sales channel.
-
-Notice that the request object's type is `MedusaStoreRequest` instead of `MedusaRequest` to ensure the availability of the `publishable_key_context` property.
-
-### Method 2: Use Product's Sales Channel
-
-A product is linked to the sales channels it's available in. So, you can retrieve the details of the variant's product, including its sales channels.
-
-For example:
-
-```ts highlights={productSalesChannelHighlights}
-import { getVariantAvailability } from "@medusajs/framework/utils"
-
-// ...
-
-// use req.scope instead of container in API routes
-const query = container.resolve("query")
-
-const { data: variants } = await query.graph({
- entity: "variant",
- fields: ["id", "product.sales_channels.*"],
- filters: {
- id: "variant_123",
- },
-})
-
-const availability = await getVariantAvailability(query, {
- variant_ids: ["variant_123"],
- sales_channel_id: variants[0].product!.sales_channels![0]!.id,
-})
-```
-
-In this example, you retrieve the sales channels of the variant's product using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md).
-
-You pass the ID of the variant as a filter, and you specify `product.sales_channels.*` as the fields to retrieve. This retrieves the sales channels linked to the variant's product.
-
-Then, you pass the first sales channel ID to the `getVariantAvailability` function to retrieve the inventory availability of the product variant in that sales channel.
-
-
# Calculate Product Variant Price with Taxes
In this document, you'll learn how to calculate a product variant's price with taxes.
@@ -30911,6 +30789,159 @@ For each product variant, you:
- `priceWithoutTax`: The variant's price without taxes applied.
+# Get Product Variant Inventory Quantity
+
+In this guide, you'll learn how to retrieve the available inventory quantity of a product variant in your Medusa application customizations. That includes API routes, workflows, subscribers, scheduled jobs, and any resource that can access the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md).
+
+Refer to the [Retrieve Product Variant Inventory](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/products/inventory/index.html.md) storefront guide.
+
+## Understanding Product Variant Inventory Availability
+
+Product variants have a `manage_inventory` boolean field that indicates whether the Medusa application manages the inventory of the product variant.
+
+When `manage_inventory` is disabled, the Medusa application always considers the product variant to be in stock. So, you can't retrieve the inventory quantity for those products.
+
+When `manage_inventory` is enabled, the Medusa application tracks the inventory of the product variant using the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md). For example, when a customer purchases a product variant, the Medusa application decrements the stocked quantity of the product variant.
+
+This guide explains how to retrieve the inventory quantity of a product variant when `manage_inventory` is enabled.
+
+***
+
+## Retrieve Product Variant Inventory
+
+To retrieve the inventory quantity of a product variant, use the `getVariantAvailability` utility function imported from `@medusajs/framework/utils`. It returns the available quantity of the product variant.
+
+For example:
+
+```ts highlights={variantAvailabilityHighlights}
+import { getVariantAvailability } from "@medusajs/framework/utils"
+
+// ...
+
+// use req.scope instead of container in API routes
+const query = container.resolve("query")
+
+const availability = await getVariantAvailability(query, {
+ variant_ids: ["variant_123"],
+ sales_channel_id: "sc_123",
+})
+```
+
+A product variant's inventory quantity is set per [stock location](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/index.html.md). This stock location is linked to a [sales channel](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/index.html.md).
+
+So, to retrieve the inventory quantity of a product variant using `getVariantAvailability`, you need to also provide the ID of the sales channel to retrieve the inventory quantity in.
+
+Refer to the [Retrieve Sales Channel to Use](#retrieve-sales-channel-to-use) section to learn how to retrieve the sales channel ID to use in the `getVariantAvailability` function.
+
+### Parameters
+
+The `getVariantAvailability` function accepts the following parameters:
+
+- query: (Query) Instance of Query to retrieve the necessary data.
+- options: (\`object\`) The options to retrieve the variant availability.
+
+ - variant\_ids: (\`string\[]\`) The IDs of the product variants to retrieve their inventory availability.
+
+ - sales\_channel\_id: (\`string\`) The ID of the sales channel to retrieve the variant availability in.
+
+### Returns
+
+The `getVariantAvailability` function resolves to an object whose keys are the IDs of each product variant passed in the `variant_ids` parameter.
+
+The value of each key is an object with the following properties:
+
+- availability: (\`number\`) The available quantity of the product variant in the stock location linked to the sales channel. If \`manage\_inventory\` is disabled, this value is \`0\`.
+- sales\_channel\_id: (\`string\`) The ID of the sales channel that the availability is scoped to.
+
+For example, the object may look like this:
+
+```json title="Example result"
+{
+ "variant_123": {
+ "availability": 10,
+ "sales_channel_id": "sc_123"
+ }
+}
+```
+
+***
+
+## Retrieve Sales Channel to Use
+
+To retrieve the sales channel ID to use in the `getVariantAvailability` function, you can either:
+
+- Use the sales channel of the request's scope.
+- Use the sales channel that the variant's product is available in.
+
+### Method 1: Use Sales Channel Scope in Store Routes
+
+Requests sent to API routes starting with `/store` must include a [publishable API key in the request header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/publishable-api-keys/index.html.md). This scopes the request to one or more sales channels associated with the publishable API key.
+
+So, if you're retrieving the variant inventory availability in an API route starting with `/store`, you can access the sales channel using the `publishable_key_context.sales_channel_ids` property of the request object:
+
+```ts highlights={salesChannelScopeHighlights}
+import { MedusaStoreRequest, MedusaResponse } from "@medusajs/framework/http"
+import { getVariantAvailability } from "@medusajs/framework/utils"
+
+export async function GET(
+ req: MedusaStoreRequest,
+ res: MedusaResponse
+) {
+ const query = req.scope.resolve("query")
+ const sales_channel_ids = req.publishable_key_context.sales_channel_ids
+
+ const availability = await getVariantAvailability(query, {
+ variant_ids: ["variant_123"],
+ sales_channel_id: sales_channel_ids[0],
+ })
+
+ res.json({
+ availability,
+ })
+}
+```
+
+In this example, you retrieve the scope's sales channel IDs using `req.publishable_key_context.sales_channel_ids`, whose value is an array of IDs.
+
+Then, you pass the first sales channel ID to the `getVariantAvailability` function to retrieve the inventory availability of the product variant in that sales channel.
+
+Notice that the request object's type is `MedusaStoreRequest` instead of `MedusaRequest` to ensure the availability of the `publishable_key_context` property.
+
+### Method 2: Use Product's Sales Channel
+
+A product is linked to the sales channels it's available in. So, you can retrieve the details of the variant's product, including its sales channels.
+
+For example:
+
+```ts highlights={productSalesChannelHighlights}
+import { getVariantAvailability } from "@medusajs/framework/utils"
+
+// ...
+
+// use req.scope instead of container in API routes
+const query = container.resolve("query")
+
+const { data: variants } = await query.graph({
+ entity: "variant",
+ fields: ["id", "product.sales_channels.*"],
+ filters: {
+ id: "variant_123",
+ },
+})
+
+const availability = await getVariantAvailability(query, {
+ variant_ids: ["variant_123"],
+ sales_channel_id: variants[0].product!.sales_channels![0]!.id,
+})
+```
+
+In this example, you retrieve the sales channels of the variant's product using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md).
+
+You pass the ID of the variant as a filter, and you specify `product.sales_channels.*` as the fields to retrieve. This retrieves the sales channels linked to the variant's product.
+
+Then, you pass the first sales channel ID to the `getVariantAvailability` function to retrieve the inventory availability of the product variant in that sales channel.
+
+
# GitHub Auth Module Provider
In this document, you’ll learn about the GitHub Auth Module Provider and how to install and use it in the Auth Module.
@@ -31574,76 +31605,6 @@ Medusa provides the following Event Modules. You can use one of them, or [Create
- [Redis](https://docs.medusajs.com/infrastructure-modules/event/redis/index.html.md)
-# File Module
-
-In this document, you'll learn about the File Module and its providers.
-
-## What is the File Module?
-
-The File Module exposes the functionalities to upload assets, such as product images, to the Medusa application. Medusa uses the File Module in its core commerce features for all file operations, and you can use it in your custom features as well.
-
-***
-
-## How to Use the File Module?
-
-You can use the File Module as part of the [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) you build for your custom features. A workflow is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.
-
-In a step of your workflow, you can resolve the File Module's service and use its methods to upload files, retrieve files, or delete files.
-
-For example:
-
-```ts
-import { Modules } from "@medusajs/framework/utils"
-import {
- createStep,
- createWorkflow,
- StepResponse,
- WorkflowResponse,
-} from "@medusajs/framework/workflows-sdk"
-
-const step1 = createStep(
- "step-1",
- async ({}, { container }) => {
- const fileModuleService = container.resolve(
- Modules.FILE
- )
-
- const { url } = await fileModuleService.retrieveFile("image.png")
-
- return new StepResponse(url)
- }
-)
-
-export const workflow = createWorkflow(
- "workflow-1",
- () => {
- const url = step1()
-
- return new WorkflowResponse(url)
- }
-)
-```
-
-In the example above, you create a workflow that has a step. In the step, you resolve the service of the File Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md).
-
-Then, you use the `retrieveFile` method of the File Module to retrieve the URL of the file with the name `"image.png"`. The URL is then returned as a response from the step and the workflow.
-
-***
-
-### What is a File Module Provider?
-
-A File Module Provider implements the underlying logic of handling uploads and downloads of assets, such as integrating third-party services. The File Module then uses the registered File Module Provider to handle file operations.
-
-Only one File Module Provider can be registered at a time. If you register multiple providers, the File Module will throw an error.
-
-By default, Medusa uses the [Local File Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/file/local/index.html.md). This module uploads files to the `uploads` directory of your Medusa application.
-
-This is useful for development. However, for production, it’s highly recommended to use other File Module Providers, such as the S3 File Module Provider. You can also [Create a File Provider](https://docs.medusajs.com/references/file-provider-module/index.html.md).
-
-- [Local](https://docs.medusajs.com/infrastructure-modules/file/local/index.html.md)
-- [AWS S3 (and Compatible APIs)](https://docs.medusajs.com/infrastructure-modules/file/s3/index.html.md)
-
-
# Locking Module
In this document, you'll learn about the Locking Module and its providers.
@@ -31757,6 +31718,76 @@ Medusa provides the following Locking Module Providers. You can use one of them,
- [PostgreSQL](https://docs.medusajs.com/infrastructure-modules/locking/postgres/index.html.md)
+# File Module
+
+In this document, you'll learn about the File Module and its providers.
+
+## What is the File Module?
+
+The File Module exposes the functionalities to upload assets, such as product images, to the Medusa application. Medusa uses the File Module in its core commerce features for all file operations, and you can use it in your custom features as well.
+
+***
+
+## How to Use the File Module?
+
+You can use the File Module as part of the [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) you build for your custom features. A workflow is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.
+
+In a step of your workflow, you can resolve the File Module's service and use its methods to upload files, retrieve files, or delete files.
+
+For example:
+
+```ts
+import { Modules } from "@medusajs/framework/utils"
+import {
+ createStep,
+ createWorkflow,
+ StepResponse,
+ WorkflowResponse,
+} from "@medusajs/framework/workflows-sdk"
+
+const step1 = createStep(
+ "step-1",
+ async ({}, { container }) => {
+ const fileModuleService = container.resolve(
+ Modules.FILE
+ )
+
+ const { url } = await fileModuleService.retrieveFile("image.png")
+
+ return new StepResponse(url)
+ }
+)
+
+export const workflow = createWorkflow(
+ "workflow-1",
+ () => {
+ const url = step1()
+
+ return new WorkflowResponse(url)
+ }
+)
+```
+
+In the example above, you create a workflow that has a step. In the step, you resolve the service of the File Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md).
+
+Then, you use the `retrieveFile` method of the File Module to retrieve the URL of the file with the name `"image.png"`. The URL is then returned as a response from the step and the workflow.
+
+***
+
+### What is a File Module Provider?
+
+A File Module Provider implements the underlying logic of handling uploads and downloads of assets, such as integrating third-party services. The File Module then uses the registered File Module Provider to handle file operations.
+
+Only one File Module Provider can be registered at a time. If you register multiple providers, the File Module will throw an error.
+
+By default, Medusa uses the [Local File Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/file/local/index.html.md). This module uploads files to the `uploads` directory of your Medusa application.
+
+This is useful for development. However, for production, it’s highly recommended to use other File Module Providers, such as the S3 File Module Provider. You can also [Create a File Provider](https://docs.medusajs.com/references/file-provider-module/index.html.md).
+
+- [Local](https://docs.medusajs.com/infrastructure-modules/file/local/index.html.md)
+- [AWS S3 (and Compatible APIs)](https://docs.medusajs.com/infrastructure-modules/file/s3/index.html.md)
+
+
# Workflow Engine Module
In this document, you'll learn what a Workflow Engine Module is and how to use it in your Medusa application.
@@ -32136,6 +32167,46 @@ You'll now track the order creation event whenever an order is placed in your Me
- [How to Use the Analytics Module](https://docs.medusajs.com/references/analytics/service/index.html.md)
+# In-Memory Cache Module
+
+The In-Memory Cache Module uses a plain JavaScript Map object to store the cached data. This module is used by default in your Medusa application.
+
+This module is helpful for development or when you’re testing out Medusa, but it’s not recommended to be used in production.
+
+For production, it’s recommended to use modules like [Redis Cache Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/redis/index.html.md).
+
+***
+
+## Register the In-Memory Cache Module
+
+The In-Memory Cache Module is registered by default in your application.
+
+Add the module into the `modules` property of the exported object in `medusa-config.ts`:
+
+```ts title="medusa-config.ts"
+import { Modules } from "@medusajs/framework/utils"
+// ...
+
+module.exports = defineConfig({
+ // ...
+ modules: [
+ {
+ resolve: "@medusajs/medusa/cache-inmemory",
+ options: {
+ // optional options
+ },
+ },
+ ],
+})
+```
+
+### In-Memory Cache Module Options
+
+|Option|Description|Default|
+|---|---|---|---|---|
+|\`ttl\`|The number of seconds an item can live in the cache before it’s removed.|\`30\`|
+
+
# How to Create a Cache Module
In this guide, you’ll learn how to create a Cache Module.
@@ -32306,6 +32377,55 @@ module.exports = defineConfig({
```
+# Local Notification Module Provider
+
+The Local Notification Module Provider simulates sending a notification, but only logs the notification's details in the terminal. This is useful for development.
+
+***
+
+## Register the Local Notification Module
+
+The Local Notification Module Provider is registered by default in your application. It's configured to run on the `feed` channel.
+
+Add the module into the `providers` array of the Notification Module:
+
+Only one provider can be defined for a channel.
+
+```ts title="medusa-config.ts"
+import { Modules } from "@medusajs/framework/utils"
+
+// ...
+
+module.exports = defineConfig({
+ // ...
+ modules: [
+ {
+ resolve: "@medusajs/medusa/notification",
+ options: {
+ providers: [
+ // ...
+ {
+ resolve: "@medusajs/medusa/notification-local",
+ id: "local",
+ options: {
+ channels: ["email"],
+ },
+ },
+ ],
+ },
+ },
+ ],
+})
+```
+
+### Local Notification Module Options
+
+|Option|Description|
+|---|---|---|
+|\`channels\`|The channels this notification module is used to send notifications for. While the local notification module doesn't actually send the notification,
+it's important to specify its channels to make sure it's used when a notification for that channel is created.|
+
+
# Redis Cache Module
The Redis Cache Module uses Redis to cache data in your store. In production, it's recommended to use this module.
@@ -32372,95 +32492,6 @@ Connection to Redis in module 'cache-redis' established
```
-# In-Memory Cache Module
-
-The In-Memory Cache Module uses a plain JavaScript Map object to store the cached data. This module is used by default in your Medusa application.
-
-This module is helpful for development or when you’re testing out Medusa, but it’s not recommended to be used in production.
-
-For production, it’s recommended to use modules like [Redis Cache Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/redis/index.html.md).
-
-***
-
-## Register the In-Memory Cache Module
-
-The In-Memory Cache Module is registered by default in your application.
-
-Add the module into the `modules` property of the exported object in `medusa-config.ts`:
-
-```ts title="medusa-config.ts"
-import { Modules } from "@medusajs/framework/utils"
-// ...
-
-module.exports = defineConfig({
- // ...
- modules: [
- {
- resolve: "@medusajs/medusa/cache-inmemory",
- options: {
- // optional options
- },
- },
- ],
-})
-```
-
-### In-Memory Cache Module Options
-
-|Option|Description|Default|
-|---|---|---|---|---|
-|\`ttl\`|The number of seconds an item can live in the cache before it’s removed.|\`30\`|
-
-
-# Local Notification Module Provider
-
-The Local Notification Module Provider simulates sending a notification, but only logs the notification's details in the terminal. This is useful for development.
-
-***
-
-## Register the Local Notification Module
-
-The Local Notification Module Provider is registered by default in your application. It's configured to run on the `feed` channel.
-
-Add the module into the `providers` array of the Notification Module:
-
-Only one provider can be defined for a channel.
-
-```ts title="medusa-config.ts"
-import { Modules } from "@medusajs/framework/utils"
-
-// ...
-
-module.exports = defineConfig({
- // ...
- modules: [
- {
- resolve: "@medusajs/medusa/notification",
- options: {
- providers: [
- // ...
- {
- resolve: "@medusajs/medusa/notification-local",
- id: "local",
- options: {
- channels: ["email"],
- },
- },
- ],
- },
- },
- ],
-})
-```
-
-### Local Notification Module Options
-
-|Option|Description|
-|---|---|---|
-|\`channels\`|The channels this notification module is used to send notifications for. While the local notification module doesn't actually send the notification,
-it's important to specify its channels to make sure it's used when a notification for that channel is created.|
-
-
# Send Notification with the Notification Module
In this guide, you'll learn about the different ways to send notifications using the Notification Module.
@@ -33133,57 +33164,6 @@ Connection to Redis in module 'event-redis' established
```
-# Local File Module Provider
-
-The Local File Module Provider stores files uploaded to your Medusa application in the `/uploads` directory.
-
-- The Local File Module Provider is only for development purposes. Use the [S3 File Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/file/s3/index.html.md) in production instead.
-- The Local File Module Provider will only read files uploaded through Medusa. It will not read files uploaded manually to the `static` (or other configured) directory.
-
-***
-
-## Register the Local File Module
-
-The Local File Module Provider is registered by default in your application.
-
-Add the module into the `providers` array of the File Module:
-
-The File Module accepts one provider only.
-
-```ts title="medusa-config.ts"
-import { Modules } from "@medusajs/framework/utils"
-
-// ...
-
-module.exports = {
- // ...
- modules: [
- {
- resolve: "@medusajs/medusa/file",
- options: {
- providers: [
- {
- resolve: "@medusajs/medusa/file-local",
- id: "local",
- options: {
- // provider options...
- },
- },
- ],
- },
- },
- ],
-}
-```
-
-### Local File Module Options
-
-|Option|Description|Default|
-|---|---|---|---|---|
-|\`upload\_dir\`|The directory to upload files to. Medusa exposes the content of the |\`static\`|
-|\`backend\_url\`|The URL that serves the files.|\`http://localhost:9000/static\`|
-
-
# PostgreSQL Locking Module Provider
The PostgreSQL Locking Module Provider uses PostgreSQL's advisory locks to control and manage locks across multiple instances of Medusa. Advisory locks are lightweight locks that do not interfere with other database transactions. By using PostgreSQL's advisory locks, Medusa can create distributed locks directly through the database.
@@ -33279,6 +33259,193 @@ const step1 = createStep(
```
+# Redis Locking Module Provider
+
+The Redis Locking Module Provider uses Redis to manage locks across multiple instances of Medusa. Redis ensures that locks are globally available, which is ideal for distributed environments.
+
+This provider is recommended for production environments where Medusa is running in a multi-instance setup.
+
+***
+
+## Register the Redis Locking Module Provider
+
+### Prerequisites
+
+- [A redis server set up locally or a database in your deployed application.](https://redis.io/download)
+
+To register the Redis Locking Module Provider, add it to the list of providers of the Locking Module in `medusa-config.ts`:
+
+```ts title="medusa-config.ts"
+module.exports = defineConfig({
+ // ...
+ modules: [
+ {
+ resolve: "@medusajs/medusa/locking",
+ options: {
+ providers: [
+ {
+ resolve: "@medusajs/medusa/locking-redis",
+ id: "locking-redis",
+ // set this if you want this provider to be used by default
+ // and you have other Locking Module Providers registered.
+ is_default: true,
+ options: {
+ redisUrl: process.env.LOCKING_REDIS_URL,
+ },
+ },
+ ],
+ },
+ },
+ ],
+})
+```
+
+### Environment Variables
+
+Make sure to add the following environment variable:
+
+```bash
+LOCKING_REDIS_URL=
+```
+
+Where `` is the URL of your Redis server, either locally or in the deployed environment.
+
+The default Redis URL in a local environment is `redis://localhost:6379`.
+
+### Redis Locking Module Provider Options
+
+|Option|Description|Required|Default|
+|---|---|---|---|---|---|---|
+|\`redisUrl\`|A string indicating the Redis connection URL.|Yes|-|
+|\`redisOptions\`|An object of Redis options. Refer to the |No|-|
+|\`namespace\`|A string used to prefix all locked keys with |No|\`medusa\_lock:\`|
+|\`waitLockingTimeout\`|A number indicating the default timeout (in seconds) to wait while acquiring a lock. This timeout is used when no timeout is specified when executing an asynchronous job or acquiring a lock.|No|\`5\`|
+|\`defaultRetryInterval\`|A number indicating the time (in milliseconds) to wait before retrying to acquire a lock.|No|\`5\`|
+|\`maximumRetryInterval\`|A number indicating the maximum time (in milliseconds) to wait before retrying to acquire a lock.|No|\`200\`|
+
+***
+
+## Test out the Module
+
+To test out the Redis Locking Module Provider, start the Medusa application:
+
+```bash npm2yarn
+npm run dev
+```
+
+You'll see the following message logged in the terminal:
+
+```bash
+info: Connection to Redis in "locking-redis" provider established
+```
+
+This message indicates that the Redis Locking Module Provider has successfully connected to the Redis server.
+
+If you set the `is_default` flag to `true` in the provider options or you only registered the Redis Locking Module Provider, the Locking Module will use it by default for all locking operations.
+
+***
+
+## Use Provider with Locking Module
+
+The Redis Locking Module Provider will be the default provider if you don't register any other providers, or if you set the `is_default` flag to `true`:
+
+```ts title="medusa-config.ts" highlights={defaultHighlights}
+module.exports = defineConfig({
+ // ...
+ modules: [
+ {
+ resolve: "@medusajs/medusa/locking",
+ options: {
+ providers: [
+ {
+ resolve: "@medusajs/medusa/locking-redis",
+ id: "locking-redis",
+ is_default: true,
+ options: {
+ // ...
+ },
+ },
+ ],
+ },
+ },
+ ],
+})
+```
+
+If you use the Locking Module in your customizations, the Redis Locking Module Provider will be used by default in this case. You can also explicitly use this provider by passing its identifier `lp_locking-redis` to the Locking Module's service methods.
+
+For example, when using the `acquire` method in a [workflow step](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md):
+
+```ts
+import { Modules } from "@medusajs/framework/utils"
+import { createStep } from "@medusajs/framework/workflows-sdk"
+
+const step1 = createStep(
+ "step-1",
+ async ({}, { container }) => {
+ const lockingModuleService = container.resolve(
+ Modules.LOCKING
+ )
+
+ await lockingModuleService.acquire("prod_123", {
+ provider: "lp_locking-redis",
+ })
+ }
+)
+```
+
+
+# Local File Module Provider
+
+The Local File Module Provider stores files uploaded to your Medusa application in the `/uploads` directory.
+
+- The Local File Module Provider is only for development purposes. Use the [S3 File Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/file/s3/index.html.md) in production instead.
+- The Local File Module Provider will only read files uploaded through Medusa. It will not read files uploaded manually to the `static` (or other configured) directory.
+
+***
+
+## Register the Local File Module
+
+The Local File Module Provider is registered by default in your application.
+
+Add the module into the `providers` array of the File Module:
+
+The File Module accepts one provider only.
+
+```ts title="medusa-config.ts"
+import { Modules } from "@medusajs/framework/utils"
+
+// ...
+
+module.exports = {
+ // ...
+ modules: [
+ {
+ resolve: "@medusajs/medusa/file",
+ options: {
+ providers: [
+ {
+ resolve: "@medusajs/medusa/file-local",
+ id: "local",
+ options: {
+ // provider options...
+ },
+ },
+ ],
+ },
+ },
+ ],
+}
+```
+
+### Local File Module Options
+
+|Option|Description|Default|
+|---|---|---|---|---|
+|\`upload\_dir\`|The directory to upload files to. Medusa exposes the content of the |\`static\`|
+|\`backend\_url\`|The URL that serves the files.|\`http://localhost:9000/static\`|
+
+
# S3 File Module Provider
The S3 File Module Provider integrates Amazon S3 and services following a compatible API (such as MinIO or DigitalOcean Spaces) to store files uploaded to your Medusa application.
@@ -33432,142 +33599,6 @@ module.exports = defineConfig({
## Troubleshooting
-# Redis Locking Module Provider
-
-The Redis Locking Module Provider uses Redis to manage locks across multiple instances of Medusa. Redis ensures that locks are globally available, which is ideal for distributed environments.
-
-This provider is recommended for production environments where Medusa is running in a multi-instance setup.
-
-***
-
-## Register the Redis Locking Module Provider
-
-### Prerequisites
-
-- [A redis server set up locally or a database in your deployed application.](https://redis.io/download)
-
-To register the Redis Locking Module Provider, add it to the list of providers of the Locking Module in `medusa-config.ts`:
-
-```ts title="medusa-config.ts"
-module.exports = defineConfig({
- // ...
- modules: [
- {
- resolve: "@medusajs/medusa/locking",
- options: {
- providers: [
- {
- resolve: "@medusajs/medusa/locking-redis",
- id: "locking-redis",
- // set this if you want this provider to be used by default
- // and you have other Locking Module Providers registered.
- is_default: true,
- options: {
- redisUrl: process.env.LOCKING_REDIS_URL,
- },
- },
- ],
- },
- },
- ],
-})
-```
-
-### Environment Variables
-
-Make sure to add the following environment variable:
-
-```bash
-LOCKING_REDIS_URL=
-```
-
-Where `` is the URL of your Redis server, either locally or in the deployed environment.
-
-The default Redis URL in a local environment is `redis://localhost:6379`.
-
-### Redis Locking Module Provider Options
-
-|Option|Description|Required|Default|
-|---|---|---|---|---|---|---|
-|\`redisUrl\`|A string indicating the Redis connection URL.|Yes|-|
-|\`redisOptions\`|An object of Redis options. Refer to the |No|-|
-|\`namespace\`|A string used to prefix all locked keys with |No|\`medusa\_lock:\`|
-|\`waitLockingTimeout\`|A number indicating the default timeout (in seconds) to wait while acquiring a lock. This timeout is used when no timeout is specified when executing an asynchronous job or acquiring a lock.|No|\`5\`|
-|\`defaultRetryInterval\`|A number indicating the time (in milliseconds) to wait before retrying to acquire a lock.|No|\`5\`|
-|\`maximumRetryInterval\`|A number indicating the maximum time (in milliseconds) to wait before retrying to acquire a lock.|No|\`200\`|
-
-***
-
-## Test out the Module
-
-To test out the Redis Locking Module Provider, start the Medusa application:
-
-```bash npm2yarn
-npm run dev
-```
-
-You'll see the following message logged in the terminal:
-
-```bash
-info: Connection to Redis in "locking-redis" provider established
-```
-
-This message indicates that the Redis Locking Module Provider has successfully connected to the Redis server.
-
-If you set the `is_default` flag to `true` in the provider options or you only registered the Redis Locking Module Provider, the Locking Module will use it by default for all locking operations.
-
-***
-
-## Use Provider with Locking Module
-
-The Redis Locking Module Provider will be the default provider if you don't register any other providers, or if you set the `is_default` flag to `true`:
-
-```ts title="medusa-config.ts" highlights={defaultHighlights}
-module.exports = defineConfig({
- // ...
- modules: [
- {
- resolve: "@medusajs/medusa/locking",
- options: {
- providers: [
- {
- resolve: "@medusajs/medusa/locking-redis",
- id: "locking-redis",
- is_default: true,
- options: {
- // ...
- },
- },
- ],
- },
- },
- ],
-})
-```
-
-If you use the Locking Module in your customizations, the Redis Locking Module Provider will be used by default in this case. You can also explicitly use this provider by passing its identifier `lp_locking-redis` to the Locking Module's service methods.
-
-For example, when using the `acquire` method in a [workflow step](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md):
-
-```ts
-import { Modules } from "@medusajs/framework/utils"
-import { createStep } from "@medusajs/framework/workflows-sdk"
-
-const step1 = createStep(
- "step-1",
- async ({}, { container }) => {
- const lockingModuleService = container.resolve(
- Modules.LOCKING
- )
-
- await lockingModuleService.acquire("prod_123", {
- provider: "lp_locking-redis",
- })
- }
-)
-```
-
-
# How to Use the Workflow Engine Module
In this document, you’ll learn about the different methods in the Workflow Engine Module's service and how to use them.
@@ -33757,6 +33788,38 @@ await workflowEngineModuleService.unsubscribe({
- subscriberOrId: (\`string\`) The subscriber ID or the subscriber function to unsubscribe from the workflow's events.
+# In-Memory Workflow Engine Module
+
+The In-Memory Workflow Engine Module uses a plain JavaScript Map object to store the workflow executions.
+
+This module is helpful for development or when you’re testing out Medusa, but it’s not recommended to be used in production.
+
+For production, it’s recommended to use modules like [Redis Workflow Engine Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/redis/index.html.md).
+
+***
+
+## Register the In-Memory Workflow Engine Module
+
+The In-Memory Workflow Engine Module is registered by default in your application.
+
+Add the module into the `modules` property of the exported object in `medusa-config.ts`:
+
+```ts title="medusa-config.ts"
+import { Modules } from "@medusajs/framework/utils"
+
+// ...
+
+module.exports = defineConfig({
+ // ...
+ modules: [
+ {
+ resolve: "@medusajs/medusa/workflow-engine-inmemory",
+ },
+ ],
+})
+```
+
+
# Redis Workflow Engine Module
The Redis Workflow Engine Module uses Redis to track workflow executions and handle their subscribers. In production, it's recommended to use this module.
@@ -33823,664 +33886,632 @@ Connection to Redis in module 'workflow-engine-redis' established
```
-# In-Memory Workflow Engine Module
-
-The In-Memory Workflow Engine Module uses a plain JavaScript Map object to store the workflow executions.
-
-This module is helpful for development or when you’re testing out Medusa, but it’s not recommended to be used in production.
-
-For production, it’s recommended to use modules like [Redis Workflow Engine Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/redis/index.html.md).
-
-***
-
-## Register the In-Memory Workflow Engine Module
-
-The In-Memory Workflow Engine Module is registered by default in your application.
-
-Add the module into the `modules` property of the exported object in `medusa-config.ts`:
-
-```ts title="medusa-config.ts"
-import { Modules } from "@medusajs/framework/utils"
-
-// ...
-
-module.exports = defineConfig({
- // ...
- modules: [
- {
- resolve: "@medusajs/medusa/workflow-engine-inmemory",
- },
- ],
-})
-```
-
-
## Workflows
-- [createApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/createApiKeysWorkflow/index.html.md)
-- [deleteApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteApiKeysWorkflow/index.html.md)
- [linkSalesChannelsToApiKeyWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkSalesChannelsToApiKeyWorkflow/index.html.md)
+- [createApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/createApiKeysWorkflow/index.html.md)
- [revokeApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/revokeApiKeysWorkflow/index.html.md)
+- [deleteApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteApiKeysWorkflow/index.html.md)
- [updateApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateApiKeysWorkflow/index.html.md)
-- [createLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/createLinksWorkflow/index.html.md)
+- [addShippingMethodToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addShippingMethodToCartWorkflow/index.html.md)
+- [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md)
+- [confirmVariantInventoryWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmVariantInventoryWorkflow/index.html.md)
+- [createCartCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartCreditLinesWorkflow/index.html.md)
+- [createCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartWorkflow/index.html.md)
+- [deleteCartCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCartCreditLinesWorkflow/index.html.md)
+- [createPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentCollectionForCartWorkflow/index.html.md)
+- [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/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)
+- [refreshPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshPaymentCollectionForCartWorkflow/index.html.md)
+- [transferCartCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/transferCartCustomerWorkflow/index.html.md)
+- [refundPaymentAndRecreatePaymentSessionWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentAndRecreatePaymentSessionWorkflow/index.html.md)
+- [updateCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartWorkflow/index.html.md)
+- [updateLineItemInCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateLineItemInCartWorkflow/index.html.md)
+- [refreshCartShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartShippingMethodsWorkflow/index.html.md)
+- [updateCartPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartPromotionsWorkflow/index.html.md)
+- [updateTaxLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxLinesWorkflow/index.html.md)
+- [validateExistingPaymentCollectionStep](https://docs.medusajs.com/references/medusa-workflows/validateExistingPaymentCollectionStep/index.html.md)
- [batchLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinksWorkflow/index.html.md)
-- [updateLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateLinksWorkflow/index.html.md)
+- [createLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/createLinksWorkflow/index.html.md)
- [dismissLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/dismissLinksWorkflow/index.html.md)
-- [generateResetPasswordTokenWorkflow](https://docs.medusajs.com/references/medusa-workflows/generateResetPasswordTokenWorkflow/index.html.md)
-- [createCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAccountWorkflow/index.html.md)
-- [createCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAddressesWorkflow/index.html.md)
-- [createCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomersWorkflow/index.html.md)
-- [deleteCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomerAddressesWorkflow/index.html.md)
-- [deleteCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomersWorkflow/index.html.md)
-- [removeCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeCustomerAccountWorkflow/index.html.md)
-- [updateCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerAddressesWorkflow/index.html.md)
-- [updateCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomersWorkflow/index.html.md)
+- [updateLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateLinksWorkflow/index.html.md)
+- [createDefaultsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createDefaultsWorkflow/index.html.md)
+- [createCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerGroupsWorkflow/index.html.md)
+- [deleteCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomerGroupsWorkflow/index.html.md)
+- [linkCustomerGroupsToCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkCustomerGroupsToCustomerWorkflow/index.html.md)
+- [linkCustomersToCustomerGroupWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkCustomersToCustomerGroupWorkflow/index.html.md)
+- [updateCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerGroupsWorkflow/index.html.md)
+- [deleteFilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteFilesWorkflow/index.html.md)
+- [uploadFilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/uploadFilesWorkflow/index.html.md)
- [addDraftOrderItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addDraftOrderItemsWorkflow/index.html.md)
- [addDraftOrderPromotionWorkflow](https://docs.medusajs.com/references/medusa-workflows/addDraftOrderPromotionWorkflow/index.html.md)
-- [addDraftOrderShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addDraftOrderShippingMethodsWorkflow/index.html.md)
-- [confirmDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmDraftOrderEditWorkflow/index.html.md)
- [cancelDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelDraftOrderEditWorkflow/index.html.md)
+- [confirmDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmDraftOrderEditWorkflow/index.html.md)
+- [addDraftOrderShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addDraftOrderShippingMethodsWorkflow/index.html.md)
- [beginDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginDraftOrderEditWorkflow/index.html.md)
-- [convertDraftOrderStep](https://docs.medusajs.com/references/medusa-workflows/convertDraftOrderStep/index.html.md)
- [convertDraftOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/convertDraftOrderWorkflow/index.html.md)
- [removeDraftOrderActionItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderActionItemWorkflow/index.html.md)
+- [convertDraftOrderStep](https://docs.medusajs.com/references/medusa-workflows/convertDraftOrderStep/index.html.md)
- [removeDraftOrderActionShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderActionShippingMethodWorkflow/index.html.md)
- [removeDraftOrderPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderPromotionsWorkflow/index.html.md)
-- [requestDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestDraftOrderEditWorkflow/index.html.md)
- [removeDraftOrderShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderShippingMethodWorkflow/index.html.md)
- [updateDraftOrderActionItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderActionItemWorkflow/index.html.md)
- [updateDraftOrderActionShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderActionShippingMethodWorkflow/index.html.md)
- [updateDraftOrderItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderItemWorkflow/index.html.md)
- [updateDraftOrderShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderShippingMethodWorkflow/index.html.md)
-- [updateDraftOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderWorkflow/index.html.md)
- [updateDraftOrderStep](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderStep/index.html.md)
-- [createDefaultsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createDefaultsWorkflow/index.html.md)
-- [addShippingMethodToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addShippingMethodToCartWorkflow/index.html.md)
-- [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md)
-- [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md)
-- [createCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartWorkflow/index.html.md)
-- [confirmVariantInventoryWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmVariantInventoryWorkflow/index.html.md)
-- [createCartCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartCreditLinesWorkflow/index.html.md)
-- [createPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentCollectionForCartWorkflow/index.html.md)
-- [listShippingOptionsForCartWithPricingWorkflow](https://docs.medusajs.com/references/medusa-workflows/listShippingOptionsForCartWithPricingWorkflow/index.html.md)
-- [refreshCartItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartItemsWorkflow/index.html.md)
-- [listShippingOptionsForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/listShippingOptionsForCartWorkflow/index.html.md)
-- [deleteCartCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCartCreditLinesWorkflow/index.html.md)
-- [refreshCartShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartShippingMethodsWorkflow/index.html.md)
-- [refreshPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshPaymentCollectionForCartWorkflow/index.html.md)
-- [transferCartCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/transferCartCustomerWorkflow/index.html.md)
-- [updateCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartWorkflow/index.html.md)
-- [refundPaymentAndRecreatePaymentSessionWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentAndRecreatePaymentSessionWorkflow/index.html.md)
-- [updateCartPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartPromotionsWorkflow/index.html.md)
-- [updateLineItemInCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateLineItemInCartWorkflow/index.html.md)
-- [updateTaxLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxLinesWorkflow/index.html.md)
-- [validateExistingPaymentCollectionStep](https://docs.medusajs.com/references/medusa-workflows/validateExistingPaymentCollectionStep/index.html.md)
-- [createCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerGroupsWorkflow/index.html.md)
-- [linkCustomerGroupsToCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkCustomerGroupsToCustomerWorkflow/index.html.md)
-- [linkCustomersToCustomerGroupWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkCustomersToCustomerGroupWorkflow/index.html.md)
-- [deleteCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomerGroupsWorkflow/index.html.md)
-- [updateCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerGroupsWorkflow/index.html.md)
-- [batchShippingOptionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchShippingOptionRulesWorkflow/index.html.md)
-- [createFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createFulfillmentWorkflow/index.html.md)
-- [calculateShippingOptionsPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/calculateShippingOptionsPricesWorkflow/index.html.md)
+- [updateDraftOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderWorkflow/index.html.md)
+- [requestDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestDraftOrderEditWorkflow/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)
+- [createFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createFulfillmentWorkflow/index.html.md)
- [createReturnFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnFulfillmentWorkflow/index.html.md)
-- [createServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createServiceZonesWorkflow/index.html.md)
-- [createShippingProfilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShippingProfilesWorkflow/index.html.md)
- [createShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShippingOptionsWorkflow/index.html.md)
- [createShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShipmentWorkflow/index.html.md)
+- [createServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createServiceZonesWorkflow/index.html.md)
+- [createShippingProfilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShippingProfilesWorkflow/index.html.md)
- [deleteServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteServiceZonesWorkflow/index.html.md)
- [deleteFulfillmentSetsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteFulfillmentSetsWorkflow/index.html.md)
- [deleteShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteShippingOptionsWorkflow/index.html.md)
-- [markFulfillmentAsDeliveredWorkflow](https://docs.medusajs.com/references/medusa-workflows/markFulfillmentAsDeliveredWorkflow/index.html.md)
- [updateServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateServiceZonesWorkflow/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)
- [updateShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateShippingOptionsWorkflow/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)
-- [deleteFilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteFilesWorkflow/index.html.md)
-- [uploadFilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/uploadFilesWorkflow/index.html.md)
+- [validateFulfillmentDeliverabilityStep](https://docs.medusajs.com/references/medusa-workflows/validateFulfillmentDeliverabilityStep/index.html.md)
- [acceptInviteWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptInviteWorkflow/index.html.md)
-- [createInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInvitesWorkflow/index.html.md)
- [deleteInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInvitesWorkflow/index.html.md)
+- [createInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInvitesWorkflow/index.html.md)
- [refreshInviteTokensWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshInviteTokensWorkflow/index.html.md)
- [deleteLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteLineItemsWorkflow/index.html.md)
-- [bulkCreateDeleteLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/bulkCreateDeleteLevelsWorkflow/index.html.md)
-- [batchInventoryItemLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchInventoryItemLevelsWorkflow/index.html.md)
-- [createInventoryItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInventoryItemsWorkflow/index.html.md)
-- [createInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInventoryLevelsWorkflow/index.html.md)
-- [deleteInventoryItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInventoryItemWorkflow/index.html.md)
-- [deleteInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInventoryLevelsWorkflow/index.html.md)
-- [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)
-- [createPaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentSessionsWorkflow/index.html.md)
-- [createRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRefundReasonsWorkflow/index.html.md)
-- [deleteRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRefundReasonsWorkflow/index.html.md)
-- [updateRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRefundReasonsWorkflow/index.html.md)
-- [deletePaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePaymentSessionsWorkflow/index.html.md)
-- [capturePaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/capturePaymentWorkflow/index.html.md)
+- [generateResetPasswordTokenWorkflow](https://docs.medusajs.com/references/medusa-workflows/generateResetPasswordTokenWorkflow/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)
+- [capturePaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/capturePaymentWorkflow/index.html.md)
- [refundPaymentsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentsWorkflow/index.html.md)
- [validatePaymentsRefundStep](https://docs.medusajs.com/references/medusa-workflows/validatePaymentsRefundStep/index.html.md)
- [validateRefundStep](https://docs.medusajs.com/references/medusa-workflows/validateRefundStep/index.html.md)
+- [createCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAccountWorkflow/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)
+- [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)
+- [updateCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomersWorkflow/index.html.md)
+- [removeCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeCustomerAccountWorkflow/index.html.md)
+- [batchProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductVariantsWorkflow/index.html.md)
+- [batchLinkProductsToCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinkProductsToCollectionWorkflow/index.html.md)
+- [batchLinkProductsToCategoryWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinkProductsToCategoryWorkflow/index.html.md)
+- [createCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCollectionsWorkflow/index.html.md)
+- [createProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTagsWorkflow/index.html.md)
+- [batchProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductsWorkflow/index.html.md)
+- [createProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductOptionsWorkflow/index.html.md)
+- [createProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTypesWorkflow/index.html.md)
+- [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/index.html.md)
+- [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/index.html.md)
+- [deleteProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTagsWorkflow/index.html.md)
+- [deleteProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductOptionsWorkflow/index.html.md)
+- [deleteCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCollectionsWorkflow/index.html.md)
+- [deleteProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTypesWorkflow/index.html.md)
+- [deleteProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductVariantsWorkflow/index.html.md)
+- [exportProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/exportProductsWorkflow/index.html.md)
+- [deleteProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductsWorkflow/index.html.md)
+- [importProductsAsChunksWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsAsChunksWorkflow/index.html.md)
+- [importProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsWorkflow/index.html.md)
+- [updateCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCollectionsWorkflow/index.html.md)
+- [updateProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductOptionsWorkflow/index.html.md)
+- [updateProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTagsWorkflow/index.html.md)
+- [updateProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductsWorkflow/index.html.md)
+- [updateProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductVariantsWorkflow/index.html.md)
+- [updateProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTypesWorkflow/index.html.md)
+- [upsertVariantPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/upsertVariantPricesWorkflow/index.html.md)
+- [validateProductInputStep](https://docs.medusajs.com/references/medusa-workflows/validateProductInputStep/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)
-- [addOrderLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addOrderLineItemsWorkflow/index.html.md)
- [acceptOrderTransferWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptOrderTransferWorkflow/index.html.md)
+- [addOrderLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addOrderLineItemsWorkflow/index.html.md)
+- [archiveOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/archiveOrderWorkflow/index.html.md)
- [beginClaimOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginClaimOrderValidationStep/index.html.md)
- [beginClaimOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginClaimOrderWorkflow/index.html.md)
-- [archiveOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/archiveOrderWorkflow/index.html.md)
+- [beginExchangeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginExchangeOrderWorkflow/index.html.md)
+- [beginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginOrderExchangeValidationStep/index.html.md)
- [beginOrderEditOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditOrderWorkflow/index.html.md)
- [beginOrderEditValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditValidationStep/index.html.md)
-- [beginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginOrderExchangeValidationStep/index.html.md)
- [beginReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginReceiveReturnValidationStep/index.html.md)
- [beginReceiveReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginReceiveReturnWorkflow/index.html.md)
- [beginReturnOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginReturnOrderValidationStep/index.html.md)
-- [beginReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginReturnOrderWorkflow/index.html.md)
-- [beginExchangeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginExchangeOrderWorkflow/index.html.md)
-- [cancelBeginOrderClaimValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderClaimValidationStep/index.html.md)
- [cancelBeginOrderClaimWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderClaimWorkflow/index.html.md)
- [cancelBeginOrderEditValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditValidationStep/index.html.md)
-- [cancelBeginOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditWorkflow/index.html.md)
-- [cancelBeginOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeWorkflow/index.html.md)
-- [cancelClaimValidateOrderStep](https://docs.medusajs.com/references/medusa-workflows/cancelClaimValidateOrderStep/index.html.md)
+- [beginReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginReturnOrderWorkflow/index.html.md)
+- [cancelBeginOrderClaimValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderClaimValidationStep/index.html.md)
- [cancelBeginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeValidationStep/index.html.md)
-- [cancelExchangeValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelExchangeValidateOrder/index.html.md)
+- [cancelBeginOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditWorkflow/index.html.md)
- [cancelOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderChangeWorkflow/index.html.md)
+- [cancelClaimValidateOrderStep](https://docs.medusajs.com/references/medusa-workflows/cancelClaimValidateOrderStep/index.html.md)
+- [cancelExchangeValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelExchangeValidateOrder/index.html.md)
+- [cancelBeginOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeWorkflow/index.html.md)
- [cancelOrderClaimWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderClaimWorkflow/index.html.md)
-- [cancelOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderExchangeWorkflow/index.html.md)
- [cancelOrderFulfillmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentValidateOrder/index.html.md)
-- [cancelOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentWorkflow/index.html.md)
+- [cancelOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderExchangeWorkflow/index.html.md)
- [cancelOrderTransferRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderTransferRequestWorkflow/index.html.md)
-- [cancelRequestReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelRequestReturnValidationStep/index.html.md)
- [cancelReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelReceiveReturnValidationStep/index.html.md)
- [cancelOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderWorkflow/index.html.md)
+- [cancelOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentWorkflow/index.html.md)
+- [cancelRequestReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelRequestReturnValidationStep/index.html.md)
- [cancelReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnReceiveWorkflow/index.html.md)
-- [cancelReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnWorkflow/index.html.md)
-- [cancelTransferOrderRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelTransferOrderRequestValidationStep/index.html.md)
-- [cancelReturnValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelReturnValidateOrder/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)
- [cancelValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelValidateOrder/index.html.md)
+- [cancelTransferOrderRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelTransferOrderRequestValidationStep/index.html.md)
+- [cancelReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnWorkflow/index.html.md)
+- [completeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeOrderWorkflow/index.html.md)
+- [confirmClaimRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmClaimRequestWorkflow/index.html.md)
- [confirmClaimRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmClaimRequestValidationStep/index.html.md)
- [confirmExchangeRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestValidationStep/index.html.md)
-- [confirmClaimRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmClaimRequestWorkflow/index.html.md)
-- [completeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeOrderWorkflow/index.html.md)
- [confirmExchangeRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestWorkflow/index.html.md)
-- [confirmOrderEditRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestValidationStep/index.html.md)
- [confirmOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestWorkflow/index.html.md)
- [confirmReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmReceiveReturnValidationStep/index.html.md)
+- [confirmOrderEditRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestValidationStep/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)
- [confirmReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmReturnRequestValidationStep/index.html.md)
- [confirmReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnRequestWorkflow/index.html.md)
-- [createAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createAndCompleteReturnOrderWorkflow/index.html.md)
-- [createClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createClaimShippingMethodValidationStep/index.html.md)
- [createCompleteReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/createCompleteReturnValidationStep/index.html.md)
-- [createExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createExchangeShippingMethodValidationStep/index.html.md)
-- [createExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createExchangeShippingMethodWorkflow/index.html.md)
- [createClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createClaimShippingMethodWorkflow/index.html.md)
-- [createFulfillmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/createFulfillmentValidateOrder/index.html.md)
+- [createExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createExchangeShippingMethodValidationStep/index.html.md)
+- [createClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createClaimShippingMethodValidationStep/index.html.md)
+- [createExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createExchangeShippingMethodWorkflow/index.html.md)
- [createOrUpdateOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrUpdateOrderPaymentCollectionWorkflow/index.html.md)
+- [createFulfillmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/createFulfillmentValidateOrder/index.html.md)
- [createOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderChangeActionsWorkflow/index.html.md)
-- [createOrderCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderCreditLinesWorkflow/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)
+- [createOrderCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderCreditLinesWorkflow/index.html.md)
- [createOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderEditShippingMethodWorkflow/index.html.md)
- [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md)
- [createOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderPaymentCollectionWorkflow/index.html.md)
- [createOrderShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderShipmentWorkflow/index.html.md)
-- [createOrdersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrdersWorkflow/index.html.md)
- [createOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderWorkflow/index.html.md)
- [createReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnShippingMethodWorkflow/index.html.md)
- [createReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createReturnShippingMethodValidationStep/index.html.md)
+- [createOrdersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrdersWorkflow/index.html.md)
- [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)
-- [declineOrderTransferRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/declineOrderTransferRequestWorkflow/index.html.md)
-- [deleteOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteOrderChangeWorkflow/index.html.md)
- [deleteOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteOrderChangeActionsWorkflow/index.html.md)
- [dismissItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestValidationStep/index.html.md)
- [deleteOrderPaymentCollections](https://docs.medusajs.com/references/medusa-workflows/deleteOrderPaymentCollections/index.html.md)
+- [deleteOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteOrderChangeWorkflow/index.html.md)
- [dismissItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestWorkflow/index.html.md)
- [exchangeAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeAddNewItemValidationStep/index.html.md)
- [exchangeRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeRequestItemReturnValidationStep/index.html.md)
- [fetchShippingOptionForOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/fetchShippingOptionForOrderWorkflow/index.html.md)
- [getOrderDetailWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrderDetailWorkflow/index.html.md)
-- [getOrdersListWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrdersListWorkflow/index.html.md)
- [markPaymentCollectionAsPaid](https://docs.medusajs.com/references/medusa-workflows/markPaymentCollectionAsPaid/index.html.md)
+- [getOrdersListWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrdersListWorkflow/index.html.md)
- [markOrderFulfillmentAsDeliveredWorkflow](https://docs.medusajs.com/references/medusa-workflows/markOrderFulfillmentAsDeliveredWorkflow/index.html.md)
- [maybeRefreshShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/maybeRefreshShippingMethodsWorkflow/index.html.md)
-- [orderClaimAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimAddNewItemWorkflow/index.html.md)
-- [orderClaimAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimAddNewItemValidationStep/index.html.md)
- [orderClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimItemValidationStep/index.html.md)
+- [orderClaimAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimAddNewItemValidationStep/index.html.md)
+- [orderClaimAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimAddNewItemWorkflow/index.html.md)
- [orderClaimItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimItemWorkflow/index.html.md)
-- [orderClaimRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimRequestItemReturnValidationStep/index.html.md)
- [orderClaimRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimRequestItemReturnWorkflow/index.html.md)
+- [orderClaimRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimRequestItemReturnValidationStep/index.html.md)
+- [orderEditAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderEditAddNewItemValidationStep/index.html.md)
- [orderEditAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderEditAddNewItemWorkflow/index.html.md)
- [orderEditUpdateItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderEditUpdateItemQuantityValidationStep/index.html.md)
-- [orderEditAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderEditAddNewItemValidationStep/index.html.md)
-- [orderExchangeAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeAddNewItemWorkflow/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)
+- [orderExchangeAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeAddNewItemWorkflow/index.html.md)
- [orderExchangeRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeRequestItemReturnWorkflow/index.html.md)
+- [orderFulfillmentDeliverablilityValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderFulfillmentDeliverablilityValidationStep/index.html.md)
+- [receiveCompleteReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/receiveCompleteReturnValidationStep/index.html.md)
- [receiveAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveAndCompleteReturnOrderWorkflow/index.html.md)
- [receiveItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestValidationStep/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)
-- [removeAddItemClaimActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeAddItemClaimActionWorkflow/index.html.md)
- [removeClaimAddItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimAddItemActionValidationStep/index.html.md)
+- [receiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestWorkflow/index.html.md)
+- [removeAddItemClaimActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeAddItemClaimActionWorkflow/index.html.md)
+- [removeClaimItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimItemActionValidationStep/index.html.md)
- [removeClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimShippingMethodValidationStep/index.html.md)
- [removeExchangeItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeItemActionValidationStep/index.html.md)
-- [removeClaimItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimItemActionValidationStep/index.html.md)
- [removeClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeClaimShippingMethodWorkflow/index.html.md)
-- [removeExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeShippingMethodValidationStep/index.html.md)
-- [removeItemClaimActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemClaimActionWorkflow/index.html.md)
- [removeItemExchangeActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemExchangeActionWorkflow/index.html.md)
+- [removeItemClaimActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemClaimActionWorkflow/index.html.md)
- [removeExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeExchangeShippingMethodWorkflow/index.html.md)
-- [removeItemOrderEditActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemOrderEditActionWorkflow/index.html.md)
+- [removeExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeShippingMethodValidationStep/index.html.md)
- [removeItemReceiveReturnActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemReceiveReturnActionWorkflow/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)
- [removeItemReturnActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemReturnActionWorkflow/index.html.md)
-- [removeOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditShippingMethodValidationStep/index.html.md)
- [removeOrderEditItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditItemActionValidationStep/index.html.md)
+- [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)
-- [removeReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeReturnShippingMethodValidationStep/index.html.md)
-- [requestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestItemReturnWorkflow/index.html.md)
- [requestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestItemReturnValidationStep/index.html.md)
- [removeReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeReturnShippingMethodWorkflow/index.html.md)
+- [removeReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeReturnShippingMethodValidationStep/index.html.md)
- [requestOrderEditRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestOrderEditRequestValidationStep/index.html.md)
-- [requestOrderTransferWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestOrderTransferWorkflow/index.html.md)
-- [requestOrderTransferValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestOrderTransferValidationStep/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)
+- [requestOrderTransferValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestOrderTransferValidationStep/index.html.md)
+- [requestOrderTransferWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestOrderTransferWorkflow/index.html.md)
- [throwUnlessPaymentCollectionNotPaid](https://docs.medusajs.com/references/medusa-workflows/throwUnlessPaymentCollectionNotPaid/index.html.md)
-- [throwUnlessStatusIsNotPaid](https://docs.medusajs.com/references/medusa-workflows/throwUnlessStatusIsNotPaid/index.html.md)
- [updateClaimAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimAddItemValidationStep/index.html.md)
- [updateClaimAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimAddItemWorkflow/index.html.md)
+- [throwUnlessStatusIsNotPaid](https://docs.medusajs.com/references/medusa-workflows/throwUnlessStatusIsNotPaid/index.html.md)
+- [updateClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemValidationStep/index.html.md)
- [updateClaimItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemWorkflow/index.html.md)
- [updateClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimShippingMethodValidationStep/index.html.md)
-- [updateClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemValidationStep/index.html.md)
- [updateClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimShippingMethodWorkflow/index.html.md)
- [updateExchangeAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateExchangeAddItemValidationStep/index.html.md)
+- [updateExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateExchangeShippingMethodWorkflow/index.html.md)
+- [updateExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateExchangeShippingMethodValidationStep/index.html.md)
- [updateExchangeAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateExchangeAddItemWorkflow/index.html.md)
- [updateOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderChangeActionsWorkflow/index.html.md)
-- [updateOrderChangesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderChangesWorkflow/index.html.md)
-- [updateExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateExchangeShippingMethodValidationStep/index.html.md)
- [updateOrderEditAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditAddItemValidationStep/index.html.md)
+- [updateOrderChangesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderChangesWorkflow/index.html.md)
- [updateOrderEditAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditAddItemWorkflow/index.html.md)
-- [updateExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateExchangeShippingMethodWorkflow/index.html.md)
- [updateOrderEditItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityValidationStep/index.html.md)
-- [updateOrderEditItemQuantityWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityWorkflow/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)
- [updateOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditShippingMethodWorkflow/index.html.md)
-- [updateOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderWorkflow/index.html.md)
+- [updateOrderTaxLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderTaxLinesWorkflow/index.html.md)
- [updateOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderValidationStep/index.html.md)
+- [updateOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderWorkflow/index.html.md)
- [updateReceiveItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReceiveItemReturnRequestValidationStep/index.html.md)
- [updateReceiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReceiveItemReturnRequestWorkflow/index.html.md)
- [updateRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnValidationStep/index.html.md)
-- [updateReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReturnShippingMethodValidationStep/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)
-- [validateOrderCreditLinesStep](https://docs.medusajs.com/references/medusa-workflows/validateOrderCreditLinesStep/index.html.md)
-- [updateReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnWorkflow/index.html.md)
- [updateReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReturnValidationStep/index.html.md)
+- [updateReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnWorkflow/index.html.md)
+- [validateOrderCreditLinesStep](https://docs.medusajs.com/references/medusa-workflows/validateOrderCreditLinesStep/index.html.md)
+- [createPriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListPricesWorkflow/index.html.md)
- [batchPriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchPriceListPricesWorkflow/index.html.md)
- [createPriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListsWorkflow/index.html.md)
-- [createPriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListPricesWorkflow/index.html.md)
-- [removePriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/removePriceListPricesWorkflow/index.html.md)
- [deletePriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePriceListsWorkflow/index.html.md)
-- [updatePriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePriceListsWorkflow/index.html.md)
+- [removePriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/removePriceListPricesWorkflow/index.html.md)
- [updatePriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePriceListPricesWorkflow/index.html.md)
-- [batchProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductsWorkflow/index.html.md)
-- [batchLinkProductsToCategoryWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinkProductsToCategoryWorkflow/index.html.md)
-- [batchLinkProductsToCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinkProductsToCollectionWorkflow/index.html.md)
-- [batchProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductVariantsWorkflow/index.html.md)
-- [createProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductOptionsWorkflow/index.html.md)
-- [createCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCollectionsWorkflow/index.html.md)
-- [createProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTagsWorkflow/index.html.md)
-- [createProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTypesWorkflow/index.html.md)
-- [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/index.html.md)
-- [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/index.html.md)
-- [deleteProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTagsWorkflow/index.html.md)
-- [deleteProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTypesWorkflow/index.html.md)
-- [deleteCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCollectionsWorkflow/index.html.md)
-- [deleteProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductOptionsWorkflow/index.html.md)
-- [importProductsAsChunksWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsAsChunksWorkflow/index.html.md)
-- [deleteProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductVariantsWorkflow/index.html.md)
-- [deleteProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductsWorkflow/index.html.md)
-- [importProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsWorkflow/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)
-- [updateProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTagsWorkflow/index.html.md)
-- [updateProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductVariantsWorkflow/index.html.md)
-- [upsertVariantPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/upsertVariantPricesWorkflow/index.html.md)
-- [updateProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTypesWorkflow/index.html.md)
-- [updateProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductsWorkflow/index.html.md)
-- [validateProductInputStep](https://docs.medusajs.com/references/medusa-workflows/validateProductInputStep/index.html.md)
-- [updatePricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePricePreferencesWorkflow/index.html.md)
-- [deletePricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePricePreferencesWorkflow/index.html.md)
-- [createProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductCategoriesWorkflow/index.html.md)
-- [createPricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPricePreferencesWorkflow/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)
+- [updatePriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePriceListsWorkflow/index.html.md)
- [addOrRemoveCampaignPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addOrRemoveCampaignPromotionsWorkflow/index.html.md)
- [batchPromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchPromotionRulesWorkflow/index.html.md)
- [createCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCampaignsWorkflow/index.html.md)
-- [createPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionsWorkflow/index.html.md)
- [createPromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionRulesWorkflow/index.html.md)
-- [deleteCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCampaignsWorkflow/index.html.md)
+- [createPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionsWorkflow/index.html.md)
+- [deletePromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePromotionsWorkflow/index.html.md)
- [deletePromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePromotionRulesWorkflow/index.html.md)
+- [deleteCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCampaignsWorkflow/index.html.md)
- [updateCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCampaignsWorkflow/index.html.md)
- [updatePromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionRulesWorkflow/index.html.md)
-- [deletePromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePromotionsWorkflow/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)
+- [updatePromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsWorkflow/index.html.md)
+- [batchInventoryItemLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchInventoryItemLevelsWorkflow/index.html.md)
+- [bulkCreateDeleteLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/bulkCreateDeleteLevelsWorkflow/index.html.md)
+- [createInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInventoryLevelsWorkflow/index.html.md)
+- [deleteInventoryItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInventoryItemWorkflow/index.html.md)
+- [createInventoryItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInventoryItemsWorkflow/index.html.md)
+- [deleteInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInventoryLevelsWorkflow/index.html.md)
+- [validateInventoryLevelsDelete](https://docs.medusajs.com/references/medusa-workflows/validateInventoryLevelsDelete/index.html.md)
+- [updateInventoryItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateInventoryItemsWorkflow/index.html.md)
+- [updateInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateInventoryLevelsWorkflow/index.html.md)
- [createReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReservationsWorkflow/index.html.md)
- [deleteReservationsByLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReservationsByLineItemsWorkflow/index.html.md)
-- [deleteReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReservationsWorkflow/index.html.md)
- [updateReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReservationsWorkflow/index.html.md)
-- [createReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnReasonsWorkflow/index.html.md)
-- [deleteReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReturnReasonsWorkflow/index.html.md)
-- [updateReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnReasonsWorkflow/index.html.md)
+- [deleteReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReservationsWorkflow/index.html.md)
- [deleteRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRegionsWorkflow/index.html.md)
-- [createRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRegionsWorkflow/index.html.md)
- [updateRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRegionsWorkflow/index.html.md)
+- [createRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRegionsWorkflow/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)
- [linkProductsToSalesChannelWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkProductsToSalesChannelWorkflow/index.html.md)
- [updateSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateSalesChannelsWorkflow/index.html.md)
-- [deleteStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteStockLocationsWorkflow/index.html.md)
-- [createLocationFulfillmentSetWorkflow](https://docs.medusajs.com/references/medusa-workflows/createLocationFulfillmentSetWorkflow/index.html.md)
-- [createStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createStockLocationsWorkflow/index.html.md)
-- [linkSalesChannelsToStockLocationWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkSalesChannelsToStockLocationWorkflow/index.html.md)
-- [updateStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateStockLocationsWorkflow/index.html.md)
-- [deleteShippingProfileWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteShippingProfileWorkflow/index.html.md)
- [validateStepShippingProfileDelete](https://docs.medusajs.com/references/medusa-workflows/validateStepShippingProfileDelete/index.html.md)
-- [createStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/createStoresWorkflow/index.html.md)
-- [updateStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateStoresWorkflow/index.html.md)
-- [deleteStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteStoresWorkflow/index.html.md)
-- [createUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUserAccountWorkflow/index.html.md)
-- [deleteUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteUsersWorkflow/index.html.md)
-- [removeUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeUserAccountWorkflow/index.html.md)
-- [updateUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateUsersWorkflow/index.html.md)
-- [createUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUsersWorkflow/index.html.md)
-- [createTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRateRulesWorkflow/index.html.md)
+- [deleteShippingProfileWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteShippingProfileWorkflow/index.html.md)
+- [updatePricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePricePreferencesWorkflow/index.html.md)
+- [createPricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPricePreferencesWorkflow/index.html.md)
+- [deletePricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePricePreferencesWorkflow/index.html.md)
+- [updateProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductCategoriesWorkflow/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)
+- [createStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createStockLocationsWorkflow/index.html.md)
+- [createLocationFulfillmentSetWorkflow](https://docs.medusajs.com/references/medusa-workflows/createLocationFulfillmentSetWorkflow/index.html.md)
+- [deleteStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteStockLocationsWorkflow/index.html.md)
+- [updateStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateStockLocationsWorkflow/index.html.md)
+- [linkSalesChannelsToStockLocationWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkSalesChannelsToStockLocationWorkflow/index.html.md)
- [createTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRatesWorkflow/index.html.md)
- [createTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRegionsWorkflow/index.html.md)
+- [createTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRateRulesWorkflow/index.html.md)
- [deleteTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRateRulesWorkflow/index.html.md)
-- [deleteTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRatesWorkflow/index.html.md)
- [deleteTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRegionsWorkflow/index.html.md)
- [setTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/setTaxRateRulesWorkflow/index.html.md)
- [maybeListTaxRateRuleIdsStep](https://docs.medusajs.com/references/medusa-workflows/maybeListTaxRateRuleIdsStep/index.html.md)
+- [deleteTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRatesWorkflow/index.html.md)
- [updateTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxRatesWorkflow/index.html.md)
- [updateTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxRegionsWorkflow/index.html.md)
+- [createUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUsersWorkflow/index.html.md)
+- [createUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUserAccountWorkflow/index.html.md)
+- [deleteUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteUsersWorkflow/index.html.md)
+- [updateUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateUsersWorkflow/index.html.md)
+- [removeUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeUserAccountWorkflow/index.html.md)
+- [createStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/createStoresWorkflow/index.html.md)
+- [deleteStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteStoresWorkflow/index.html.md)
+- [updateStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateStoresWorkflow/index.html.md)
## Steps
-- [addShippingMethodToCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/addShippingMethodToCartStep/index.html.md)
-- [confirmInventoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/confirmInventoryStep/index.html.md)
-- [createLineItemAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createLineItemAdjustmentsStep/index.html.md)
-- [createCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCartsStep/index.html.md)
-- [createLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createLineItemsStep/index.html.md)
-- [findOneOrAnyRegionStep](https://docs.medusajs.com/references/medusa-workflows/steps/findOneOrAnyRegionStep/index.html.md)
-- [createShippingMethodAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingMethodAdjustmentsStep/index.html.md)
-- [createPaymentCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentCollectionsStep/index.html.md)
-- [getActionsToComputeFromPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getActionsToComputeFromPromotionsStep/index.html.md)
-- [findOrCreateCustomerStep](https://docs.medusajs.com/references/medusa-workflows/steps/findOrCreateCustomerStep/index.html.md)
-- [getPromotionCodesToApply](https://docs.medusajs.com/references/medusa-workflows/steps/getPromotionCodesToApply/index.html.md)
-- [findSalesChannelStep](https://docs.medusajs.com/references/medusa-workflows/steps/findSalesChannelStep/index.html.md)
-- [getVariantPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantPriceSetsStep/index.html.md)
-- [getLineItemActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getLineItemActionsStep/index.html.md)
-- [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)
-- [updateLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateLineItemsStep/index.html.md)
-- [updateCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartsStep/index.html.md)
-- [updateShippingMethodsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingMethodsStep/index.html.md)
-- [validateCartShippingOptionsPriceStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartShippingOptionsPriceStep/index.html.md)
-- [validateCartPaymentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartPaymentsStep/index.html.md)
-- [validateCartShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartShippingOptionsStep/index.html.md)
-- [validateLineItemPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateLineItemPricesStep/index.html.md)
-- [validateCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartStep/index.html.md)
-- [validateShippingStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShippingStep/index.html.md)
-- [validateVariantPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateVariantPricesStep/index.html.md)
-- [validateAndReturnShippingMethodsDataStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateAndReturnShippingMethodsDataStep/index.html.md)
+- [createApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/createApiKeysStep/index.html.md)
- [deleteApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteApiKeysStep/index.html.md)
- [linkSalesChannelsToApiKeyStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkSalesChannelsToApiKeyStep/index.html.md)
- [revokeApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/revokeApiKeysStep/index.html.md)
- [updateApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateApiKeysStep/index.html.md)
- [validateSalesChannelsExistStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateSalesChannelsExistStep/index.html.md)
-- [createApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/createApiKeysStep/index.html.md)
+- [setAuthAppMetadataStep](https://docs.medusajs.com/references/medusa-workflows/steps/setAuthAppMetadataStep/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)
- [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)
-- [maybeUnsetDefaultBillingAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/maybeUnsetDefaultBillingAddressesStep/index.html.md)
- [deleteCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomersStep/index.html.md)
+- [maybeUnsetDefaultBillingAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/maybeUnsetDefaultBillingAddressesStep/index.html.md)
+- [maybeUnsetDefaultShippingAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/maybeUnsetDefaultShippingAddressesStep/index.html.md)
- [updateCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomerAddressesStep/index.html.md)
- [updateCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomersStep/index.html.md)
-- [maybeUnsetDefaultShippingAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/maybeUnsetDefaultShippingAddressesStep/index.html.md)
- [validateCustomerAccountCreation](https://docs.medusajs.com/references/medusa-workflows/steps/validateCustomerAccountCreation/index.html.md)
-- [createRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRemoteLinkStep/index.html.md)
+- [validateDraftOrderStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateDraftOrderStep/index.html.md)
+- [createDefaultStoreStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultStoreStep/index.html.md)
- [createEntitiesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createEntitiesStep/index.html.md)
+- [createRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRemoteLinkStep/index.html.md)
+- [dismissRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/dismissRemoteLinkStep/index.html.md)
- [emitEventStep](https://docs.medusajs.com/references/medusa-workflows/steps/emitEventStep/index.html.md)
- [deleteEntitiesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteEntitiesStep/index.html.md)
- [updateRemoteLinksStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRemoteLinksStep/index.html.md)
-- [removeRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeRemoteLinkStep/index.html.md)
- [useQueryGraphStep](https://docs.medusajs.com/references/medusa-workflows/steps/useQueryGraphStep/index.html.md)
-- [dismissRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/dismissRemoteLinkStep/index.html.md)
+- [removeRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeRemoteLinkStep/index.html.md)
- [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)
-- [createDefaultStoreStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultStoreStep/index.html.md)
-- [createCustomerGroupsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomerGroupsStep/index.html.md)
-- [linkCustomersToCustomerGroupStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkCustomersToCustomerGroupStep/index.html.md)
-- [linkCustomerGroupsToCustomerStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkCustomerGroupsToCustomerStep/index.html.md)
-- [deleteCustomerGroupStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomerGroupStep/index.html.md)
-- [updateCustomerGroupsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomerGroupsStep/index.html.md)
-- [setAuthAppMetadataStep](https://docs.medusajs.com/references/medusa-workflows/steps/setAuthAppMetadataStep/index.html.md)
-- [validateDraftOrderStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateDraftOrderStep/index.html.md)
-- [createInventoryItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInventoryItemsStep/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)
-- [createInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInventoryLevelsStep/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)
-- [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)
-- [updateInventoryItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryItemsStep/index.html.md)
-- [validateInventoryLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryLocationsStep/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)
-- [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)
-- [createFulfillmentSets](https://docs.medusajs.com/references/medusa-workflows/steps/createFulfillmentSets/index.html.md)
-- [createShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingOptionRulesStep/index.html.md)
-- [createShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingProfilesStep/index.html.md)
-- [deleteFulfillmentSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteFulfillmentSetsStep/index.html.md)
-- [createShippingOptionsPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingOptionsPriceSetsStep/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)
-- [deleteServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteServiceZonesStep/index.html.md)
-- [updateFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateFulfillmentStep/index.html.md)
-- [updateServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateServiceZonesStep/index.html.md)
-- [deleteShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingOptionRulesStep/index.html.md)
-- [updateShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingOptionRulesStep/index.html.md)
-- [updateShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingProfilesStep/index.html.md)
-- [validateShipmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShipmentStep/index.html.md)
-- [validateShippingOptionPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShippingOptionPricesStep/index.html.md)
-- [upsertShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/upsertShippingOptionsStep/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)
+- [confirmInventoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/confirmInventoryStep/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)
+- [createLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createLineItemsStep/index.html.md)
+- [createPaymentCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentCollectionsStep/index.html.md)
+- [createShippingMethodAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingMethodAdjustmentsStep/index.html.md)
+- [findOneOrAnyRegionStep](https://docs.medusajs.com/references/medusa-workflows/steps/findOneOrAnyRegionStep/index.html.md)
+- [findOrCreateCustomerStep](https://docs.medusajs.com/references/medusa-workflows/steps/findOrCreateCustomerStep/index.html.md)
+- [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)
+- [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)
+- [getPromotionCodesToApply](https://docs.medusajs.com/references/medusa-workflows/steps/getPromotionCodesToApply/index.html.md)
+- [prepareAdjustmentsFromPromotionActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/prepareAdjustmentsFromPromotionActionsStep/index.html.md)
+- [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)
+- [getLineItemActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getLineItemActionsStep/index.html.md)
+- [removeShippingMethodFromCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeShippingMethodFromCartStep/index.html.md)
+- [reserveInventoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/reserveInventoryStep/index.html.md)
+- [retrieveCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/retrieveCartStep/index.html.md)
+- [setTaxLinesForItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/setTaxLinesForItemsStep/index.html.md)
+- [updateCartPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartPromotionsStep/index.html.md)
+- [updateCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartsStep/index.html.md)
+- [updateLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateLineItemsStep/index.html.md)
+- [updateShippingMethodsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingMethodsStep/index.html.md)
+- [validateAndReturnShippingMethodsDataStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateAndReturnShippingMethodsDataStep/index.html.md)
+- [validateCartPaymentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartPaymentsStep/index.html.md)
+- [validateCartShippingOptionsPriceStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartShippingOptionsPriceStep/index.html.md)
+- [validateCartShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartShippingOptionsStep/index.html.md)
+- [validateCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartStep/index.html.md)
+- [validateLineItemPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateLineItemPricesStep/index.html.md)
+- [validateShippingStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShippingStep/index.html.md)
+- [validateVariantPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateVariantPricesStep/index.html.md)
+- [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)
+- [createFulfillmentSets](https://docs.medusajs.com/references/medusa-workflows/steps/createFulfillmentSets/index.html.md)
+- [cancelFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelFulfillmentStep/index.html.md)
+- [createFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/createFulfillmentStep/index.html.md)
+- [createReturnFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnFulfillmentStep/index.html.md)
+- [createShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingOptionRulesStep/index.html.md)
+- [createServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createServiceZonesStep/index.html.md)
+- [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)
+- [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)
+- [deleteShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingOptionRulesStep/index.html.md)
+- [setShippingOptionsPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/setShippingOptionsPricesStep/index.html.md)
+- [updateFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateFulfillmentStep/index.html.md)
+- [updateShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingProfilesStep/index.html.md)
+- [updateServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateServiceZonesStep/index.html.md)
+- [updateShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingOptionRulesStep/index.html.md)
+- [upsertShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/upsertShippingOptionsStep/index.html.md)
+- [validateShipmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShipmentStep/index.html.md)
+- [validateShippingOptionPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShippingOptionPricesStep/index.html.md)
+- [adjustInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/adjustInventoryLevelsStep/index.html.md)
+- [createInventoryItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInventoryItemsStep/index.html.md)
+- [attachInventoryItemToVariants](https://docs.medusajs.com/references/medusa-workflows/steps/attachInventoryItemToVariants/index.html.md)
+- [createInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInventoryLevelsStep/index.html.md)
+- [deleteInventoryItemStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInventoryItemStep/index.html.md)
+- [deleteInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInventoryLevelsStep/index.html.md)
+- [updateInventoryItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryItemsStep/index.html.md)
+- [validateInventoryDeleteStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryDeleteStep/index.html.md)
+- [updateInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryLevelsStep/index.html.md)
+- [validateInventoryItemsForCreate](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryItemsForCreate/index.html.md)
+- [validateInventoryLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryLocationsStep/index.html.md)
+- [createInviteStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInviteStep/index.html.md)
+- [refreshInviteTokensStep](https://docs.medusajs.com/references/medusa-workflows/steps/refreshInviteTokensStep/index.html.md)
+- [validateTokenStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateTokenStep/index.html.md)
+- [deleteInvitesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInvitesStep/index.html.md)
+- [notifyOnFailureStep](https://docs.medusajs.com/references/medusa-workflows/steps/notifyOnFailureStep/index.html.md)
+- [sendNotificationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/sendNotificationsStep/index.html.md)
- [deleteLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteLineItemsStep/index.html.md)
-- [listLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listLineItemsStep/index.html.md)
- [updateLineItemsStepWithSelector](https://docs.medusajs.com/references/medusa-workflows/steps/updateLineItemsStepWithSelector/index.html.md)
-- [archiveOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/archiveOrdersStep/index.html.md)
+- [listLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listLineItemsStep/index.html.md)
- [addOrderTransactionStep](https://docs.medusajs.com/references/medusa-workflows/steps/addOrderTransactionStep/index.html.md)
-- [cancelOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderChangeStep/index.html.md)
-- [cancelOrderFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderFulfillmentStep/index.html.md)
-- [cancelOrderExchangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderExchangeStep/index.html.md)
-- [cancelOrderReturnStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderReturnStep/index.html.md)
+- [archiveOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/archiveOrdersStep/index.html.md)
- [cancelOrderClaimStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderClaimStep/index.html.md)
+- [cancelOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderChangeStep/index.html.md)
+- [cancelOrderExchangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderExchangeStep/index.html.md)
+- [cancelOrderFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderFulfillmentStep/index.html.md)
+- [cancelOrderReturnStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderReturnStep/index.html.md)
- [cancelOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrdersStep/index.html.md)
- [completeOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/completeOrdersStep/index.html.md)
- [createCompleteReturnStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCompleteReturnStep/index.html.md)
-- [createOrderClaimItemsFromActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderClaimItemsFromActionsStep/index.html.md)
- [createOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderChangeStep/index.html.md)
+- [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)
-- [createOrderLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderLineItemsStep/index.html.md)
- [createOrderExchangeItemsFromActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderExchangeItemsFromActionsStep/index.html.md)
- [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)
- [deleteOrderChangeActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderChangeActionsStep/index.html.md)
+- [deleteClaimsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteClaimsStep/index.html.md)
- [deleteOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderChangesStep/index.html.md)
- [deleteOrderLineItems](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderLineItems/index.html.md)
-- [deleteOrderShippingMethods](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderShippingMethods/index.html.md)
- [deleteReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReturnsStep/index.html.md)
+- [deleteOrderShippingMethods](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderShippingMethods/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)
- [registerOrderDeliveryStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderDeliveryStep/index.html.md)
- [registerOrderFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderFulfillmentStep/index.html.md)
-- [setOrderTaxLinesForItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/setOrderTaxLinesForItemsStep/index.html.md)
- [registerOrderShipmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderShipmentStep/index.html.md)
- [updateOrderChangeActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderChangeActionsStep/index.html.md)
+- [setOrderTaxLinesForItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/setOrderTaxLinesForItemsStep/index.html.md)
- [updateOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderChangesStep/index.html.md)
- [updateOrderShippingMethodsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderShippingMethodsStep/index.html.md)
- [updateOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrdersStep/index.html.md)
-- [updateReturnItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnItemsStep/index.html.md)
- [updateReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnsStep/index.html.md)
-- [sendNotificationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/sendNotificationsStep/index.html.md)
-- [notifyOnFailureStep](https://docs.medusajs.com/references/medusa-workflows/steps/notifyOnFailureStep/index.html.md)
-- [cancelPaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelPaymentStep/index.html.md)
+- [updateReturnItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnItemsStep/index.html.md)
- [authorizePaymentSessionStep](https://docs.medusajs.com/references/medusa-workflows/steps/authorizePaymentSessionStep/index.html.md)
-- [capturePaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/capturePaymentStep/index.html.md)
-- [refundPaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/refundPaymentStep/index.html.md)
+- [cancelPaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelPaymentStep/index.html.md)
- [refundPaymentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/refundPaymentsStep/index.html.md)
-- [createPaymentSessionStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentSessionStep/index.html.md)
+- [refundPaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/refundPaymentStep/index.html.md)
+- [capturePaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/capturePaymentStep/index.html.md)
- [createPaymentAccountHolderStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentAccountHolderStep/index.html.md)
-- [deleteRefundReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteRefundReasonsStep/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)
+- [deleteRefundReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteRefundReasonsStep/index.html.md)
- [deletePaymentSessionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePaymentSessionsStep/index.html.md)
- [updatePaymentCollectionStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePaymentCollectionStep/index.html.md)
- [updateRefundReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRefundReasonsStep/index.html.md)
- [validateDeletedPaymentSessionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateDeletedPaymentSessionsStep/index.html.md)
+- [createPricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPricePreferencesStep/index.html.md)
+- [createPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceSetsStep/index.html.md)
+- [updatePricePreferencesAsArrayStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePricePreferencesAsArrayStep/index.html.md)
+- [deletePricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePricePreferencesStep/index.html.md)
+- [updatePricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePricePreferencesStep/index.html.md)
+- [updatePriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceSetsStep/index.html.md)
- [createPriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceListPricesStep/index.html.md)
- [createPriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceListsStep/index.html.md)
- [deletePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePriceListsStep/index.html.md)
- [getExistingPriceListsPriceIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getExistingPriceListsPriceIdsStep/index.html.md)
-- [updatePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceListsStep/index.html.md)
- [removePriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/removePriceListPricesStep/index.html.md)
- [updatePriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceListPricesStep/index.html.md)
-- [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)
-- [createInviteStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInviteStep/index.html.md)
-- [deleteInvitesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInvitesStep/index.html.md)
-- [validateTokenStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateTokenStep/index.html.md)
-- [refreshInviteTokensStep](https://docs.medusajs.com/references/medusa-workflows/steps/refreshInviteTokensStep/index.html.md)
-- [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)
+- [updatePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceListsStep/index.html.md)
+- [validatePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validatePriceListsStep/index.html.md)
+- [batchLinkProductsToCategoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/batchLinkProductsToCategoryStep/index.html.md)
+- [batchLinkProductsToCollectionStep](https://docs.medusajs.com/references/medusa-workflows/steps/batchLinkProductsToCollectionStep/index.html.md)
+- [createCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCollectionsStep/index.html.md)
+- [createProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductOptionsStep/index.html.md)
+- [createProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductTagsStep/index.html.md)
+- [createProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductVariantsStep/index.html.md)
+- [createProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductTypesStep/index.html.md)
+- [createProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductsStep/index.html.md)
+- [createVariantPricingLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/createVariantPricingLinkStep/index.html.md)
+- [deleteCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCollectionsStep/index.html.md)
+- [deleteProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductOptionsStep/index.html.md)
+- [deleteProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductTagsStep/index.html.md)
+- [deleteProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductTypesStep/index.html.md)
+- [deleteProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductsStep/index.html.md)
+- [deleteProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductVariantsStep/index.html.md)
+- [getAllProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getAllProductsStep/index.html.md)
+- [generateProductCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/generateProductCsvStep/index.html.md)
+- [getProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getProductsStep/index.html.md)
+- [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)
+- [processImportChunksStep](https://docs.medusajs.com/references/medusa-workflows/steps/processImportChunksStep/index.html.md)
+- [normalizeCsvToChunksStep](https://docs.medusajs.com/references/medusa-workflows/steps/normalizeCsvToChunksStep/index.html.md)
+- [normalizeCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/normalizeCsvStep/index.html.md)
+- [updateCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCollectionsStep/index.html.md)
+- [updateProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductTagsStep/index.html.md)
+- [updateProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductOptionsStep/index.html.md)
+- [updateProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductTypesStep/index.html.md)
+- [updateProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductVariantsStep/index.html.md)
+- [updateProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductsStep/index.html.md)
+- [waitConfirmationProductImportStep](https://docs.medusajs.com/references/medusa-workflows/steps/waitConfirmationProductImportStep/index.html.md)
- [createProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductCategoriesStep/index.html.md)
- [deleteProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductCategoriesStep/index.html.md)
- [updateProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductCategoriesStep/index.html.md)
- [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)
+- [setRegionsPaymentProvidersStep](https://docs.medusajs.com/references/medusa-workflows/steps/setRegionsPaymentProvidersStep/index.html.md)
- [updateRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRegionsStep/index.html.md)
- [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)
- [createPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPromotionsStep/index.html.md)
- [createCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCampaignsStep/index.html.md)
-- [deletePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePromotionsStep/index.html.md)
-- [removeCampaignPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeCampaignPromotionsStep/index.html.md)
- [deleteCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCampaignsStep/index.html.md)
- [removeRulesFromPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeRulesFromPromotionsStep/index.html.md)
+- [removeCampaignPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeCampaignPromotionsStep/index.html.md)
- [updateCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCampaignsStep/index.html.md)
+- [deletePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePromotionsStep/index.html.md)
- [updatePromotionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePromotionRulesStep/index.html.md)
- [updatePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePromotionsStep/index.html.md)
- [createReturnReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnReasonsStep/index.html.md)
- [deleteReturnReasonStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReturnReasonStep/index.html.md)
- [updateReturnReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnReasonsStep/index.html.md)
-- [listShippingOptionsForContextStep](https://docs.medusajs.com/references/medusa-workflows/steps/listShippingOptionsForContextStep/index.html.md)
-- [deleteShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingProfilesStep/index.html.md)
-- [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)
-- [createDefaultSalesChannelStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultSalesChannelStep/index.html.md)
-- [createSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createSalesChannelsStep/index.html.md)
-- [detachLocationsFromSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/detachLocationsFromSalesChannelsStep/index.html.md)
-- [detachProductsFromSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/detachProductsFromSalesChannelsStep/index.html.md)
-- [updateSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateSalesChannelsStep/index.html.md)
-- [deleteSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteSalesChannelsStep/index.html.md)
-- [deleteStockLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteStockLocationsStep/index.html.md)
-- [createStockLocations](https://docs.medusajs.com/references/medusa-workflows/steps/createStockLocations/index.html.md)
-- [updateStockLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateStockLocationsStep/index.html.md)
-- [batchLinkProductsToCategoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/batchLinkProductsToCategoryStep/index.html.md)
-- [batchLinkProductsToCollectionStep](https://docs.medusajs.com/references/medusa-workflows/steps/batchLinkProductsToCollectionStep/index.html.md)
-- [createCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCollectionsStep/index.html.md)
-- [createProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductOptionsStep/index.html.md)
-- [createProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductTypesStep/index.html.md)
-- [createProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductTagsStep/index.html.md)
-- [createProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductVariantsStep/index.html.md)
-- [createProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductsStep/index.html.md)
-- [deleteCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCollectionsStep/index.html.md)
-- [createVariantPricingLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/createVariantPricingLinkStep/index.html.md)
-- [deleteProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductOptionsStep/index.html.md)
-- [deleteProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductTagsStep/index.html.md)
-- [deleteProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductTypesStep/index.html.md)
-- [deleteProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductVariantsStep/index.html.md)
-- [deleteProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductsStep/index.html.md)
-- [generateProductCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/generateProductCsvStep/index.html.md)
-- [getAllProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getAllProductsStep/index.html.md)
-- [getProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getProductsStep/index.html.md)
-- [getVariantAvailabilityStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantAvailabilityStep/index.html.md)
-- [normalizeCsvToChunksStep](https://docs.medusajs.com/references/medusa-workflows/steps/normalizeCsvToChunksStep/index.html.md)
-- [normalizeCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/normalizeCsvStep/index.html.md)
-- [parseProductCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/parseProductCsvStep/index.html.md)
-- [processImportChunksStep](https://docs.medusajs.com/references/medusa-workflows/steps/processImportChunksStep/index.html.md)
-- [updateProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductOptionsStep/index.html.md)
-- [updateCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCollectionsStep/index.html.md)
-- [updateProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductTagsStep/index.html.md)
-- [updateProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductsStep/index.html.md)
-- [updateProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductTypesStep/index.html.md)
-- [updateProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductVariantsStep/index.html.md)
-- [waitConfirmationProductImportStep](https://docs.medusajs.com/references/medusa-workflows/steps/waitConfirmationProductImportStep/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)
+- [listShippingOptionsForContextStep](https://docs.medusajs.com/references/medusa-workflows/steps/listShippingOptionsForContextStep/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)
+- [associateLocationsWithSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/associateLocationsWithSalesChannelsStep/index.html.md)
+- [createDefaultSalesChannelStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultSalesChannelStep/index.html.md)
+- [createSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createSalesChannelsStep/index.html.md)
+- [deleteSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteSalesChannelsStep/index.html.md)
+- [detachLocationsFromSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/detachLocationsFromSalesChannelsStep/index.html.md)
+- [deleteShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingProfilesStep/index.html.md)
+- [detachProductsFromSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/detachProductsFromSalesChannelsStep/index.html.md)
+- [updateSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateSalesChannelsStep/index.html.md)
+- [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)
- [createTaxRateRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRateRulesStep/index.html.md)
-- [createTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRatesStep/index.html.md)
- [createTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRegionsStep/index.html.md)
-- [deleteTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRatesStep/index.html.md)
+- [createTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRatesStep/index.html.md)
- [deleteTaxRateRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRateRulesStep/index.html.md)
+- [deleteTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRatesStep/index.html.md)
+- [getItemTaxLinesStep](https://docs.medusajs.com/references/medusa-workflows/steps/getItemTaxLinesStep/index.html.md)
- [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)
- [updateTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateTaxRatesStep/index.html.md)
-- [getItemTaxLinesStep](https://docs.medusajs.com/references/medusa-workflows/steps/getItemTaxLinesStep/index.html.md)
- [updateTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateTaxRegionsStep/index.html.md)
+- [createStoresStep](https://docs.medusajs.com/references/medusa-workflows/steps/createStoresStep/index.html.md)
- [deleteStoresStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteStoresStep/index.html.md)
- [updateStoresStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateStoresStep/index.html.md)
-- [createStoresStep](https://docs.medusajs.com/references/medusa-workflows/steps/createStoresStep/index.html.md)
- [createUsersStep](https://docs.medusajs.com/references/medusa-workflows/steps/createUsersStep/index.html.md)
- [deleteUsersStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteUsersStep/index.html.md)
- [updateUsersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateUsersStep/index.html.md)
@@ -35978,6 +36009,68 @@ npx medusa --help
***
+# build Command - Medusa CLI Reference
+
+Create a standalone build of the Medusa application.
+
+This creates a build that:
+
+- Doesn't rely on the source TypeScript files.
+- Can be copied to a production server reliably.
+
+The build is outputted to a new `.medusa/server` directory.
+
+```bash
+npx medusa build
+```
+
+Refer to [this section](#run-built-medusa-application) for next steps.
+
+## Options
+
+|Option|Description|
+|---|---|---|
+|\`--admin-only\`|Whether to only build the admin to host it separately. If this option is not passed, the admin is built to the |
+
+***
+
+## Run Built Medusa Application
+
+After running the `build` command, use the following step to run the built Medusa application:
+
+- Change to the `.medusa/server` directory and install the dependencies:
+
+```bash npm2yarn
+cd .medusa/server && npm install
+```
+
+- When running the application locally, make sure to copy the `.env` file from the root project's directory. In production, use system environment variables instead.
+
+```bash npm2yarn
+cp .env .medusa/server/.env.production
+```
+
+- In the system environment variables, set `NODE_ENV` to `production`:
+
+```bash
+NODE_ENV=production
+```
+
+- Use the `start` command to run the application:
+
+```bash npm2yarn
+cd .medusa/server && npm run start
+```
+
+***
+
+## Build Medusa Admin
+
+By default, the Medusa Admin is built to the `.medusa/server/public/admin` directory.
+
+If you want a separate build to host the admin standalone, such as on Vercel, pass the `--admin-only` option as explained in the [Options](#options) section. This outputs the admin to the `.medusa/admin` directory instead.
+
+
# db Commands - Medusa CLI Reference
Commands starting with `db:` perform actions on the database.
@@ -36159,120 +36252,6 @@ medusa new [ []]
|\`--db-host \\`|The database host to use for database setup.|
-# start Command - Medusa CLI Reference
-
-Start the Medusa application in production.
-
-```bash
-npx medusa start
-```
-
-## Options
-
-|Option|Description|Default|
-|---|---|---|---|---|
-|\`-H \\`|Set host of the Medusa server.|\`localhost\`|
-|\`-p \\`|Set port of the Medusa server.|\`9000\`|
-|\`--cluster \\`|Start Medusa's Node.js server in |Cluster mode is disabled by default. If the option is passed but no number is passed, Medusa will try to consume all available CPU cores.|
-
-
-# telemetry Command - Medusa CLI Reference
-
-Enable or disable the collection of anonymous data usage. If no option is provided, the command enables the collection of anonymous data usage.
-
-```bash
-npx medusa telemetry
-```
-
-#### Options
-
-|Option|Description|
-|---|---|---|
-|\`--enable\`|Enable telemetry (default).|
-|\`--disable\`|Disable telemetry.|
-
-
-# user Command - Medusa CLI Reference
-
-Create a new admin user.
-
-```bash
-npx medusa user --email [--password ]
-```
-
-## Options
-
-|Option|Description|Required|Default|
-|---|---|---|---|---|---|---|
-|\`-e \\`|The user's email.|Yes|-|
-|\`-p \\`|The user's password.|No|-|
-|\`-i \\`|The user's ID.|No|An automatically generated ID.|
-|\`--invite\`|Whether to create an invite instead of a user. When using this option, you don't need to specify a password.
-If ran successfully, you'll receive the invite token in the output.|No|\`false\`|
-
-
-# build Command - Medusa CLI Reference
-
-Create a standalone build of the Medusa application.
-
-This creates a build that:
-
-- Doesn't rely on the source TypeScript files.
-- Can be copied to a production server reliably.
-
-The build is outputted to a new `.medusa/server` directory.
-
-```bash
-npx medusa build
-```
-
-Refer to [this section](#run-built-medusa-application) for next steps.
-
-## Options
-
-|Option|Description|
-|---|---|---|
-|\`--admin-only\`|Whether to only build the admin to host it separately. If this option is not passed, the admin is built to the |
-
-***
-
-## Run Built Medusa Application
-
-After running the `build` command, use the following step to run the built Medusa application:
-
-- Change to the `.medusa/server` directory and install the dependencies:
-
-```bash npm2yarn
-cd .medusa/server && npm install
-```
-
-- When running the application locally, make sure to copy the `.env` file from the root project's directory. In production, use system environment variables instead.
-
-```bash npm2yarn
-cp .env .medusa/server/.env.production
-```
-
-- In the system environment variables, set `NODE_ENV` to `production`:
-
-```bash
-NODE_ENV=production
-```
-
-- Use the `start` command to run the application:
-
-```bash npm2yarn
-cd .medusa/server && npm run start
-```
-
-***
-
-## Build Medusa Admin
-
-By default, the Medusa Admin is built to the `.medusa/server/public/admin` directory.
-
-If you want a separate build to host the admin standalone, such as on Vercel, pass the `--admin-only` option as explained in the [Options](#options) section. This outputs the admin to the `.medusa/admin` directory instead.
-
-
# plugin Commands - Medusa CLI Reference
Commands starting with `plugin:` perform actions related to [plugin](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md) development.
@@ -36334,6 +36313,58 @@ npx medusa plugin:build
```
+# start Command - Medusa CLI Reference
+
+Start the Medusa application in production.
+
+```bash
+npx medusa start
+```
+
+## Options
+
+|Option|Description|Default|
+|---|---|---|---|---|
+|\`-H \\`|Set host of the Medusa server.|\`localhost\`|
+|\`-p \\`|Set port of the Medusa server.|\`9000\`|
+|\`--cluster \\`|Start Medusa's Node.js server in |Cluster mode is disabled by default. If the option is passed but no number is passed, Medusa will try to consume all available CPU cores.|
+
+
+# user Command - Medusa CLI Reference
+
+Create a new admin user.
+
+```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.|
+
+
# Medusa CLI Reference
The Medusa CLI tool provides commands that facilitate your development.
@@ -36357,6 +36388,242 @@ npx medusa --help
***
+# build Command - Medusa CLI Reference
+
+Create a standalone build of the Medusa application.
+
+This creates a build that:
+
+- Doesn't rely on the source TypeScript files.
+- Can be copied to a production server reliably.
+
+The build is outputted to a new `.medusa/server` directory.
+
+```bash
+npx medusa build
+```
+
+Refer to [this section](#run-built-medusa-application) for next steps.
+
+## Options
+
+|Option|Description|
+|---|---|---|
+|\`--admin-only\`|Whether to only build the admin to host it separately. If this option is not passed, the admin is built to the |
+
+***
+
+## Run Built Medusa Application
+
+After running the `build` command, use the following step to run the built Medusa application:
+
+- Change to the `.medusa/server` directory and install the dependencies:
+
+```bash npm2yarn
+cd .medusa/server && npm install
+```
+
+- When running the application locally, make sure to copy the `.env` file from the root project's directory. In production, use system environment variables instead.
+
+```bash npm2yarn
+cp .env .medusa/server/.env.production
+```
+
+- In the system environment variables, set `NODE_ENV` to `production`:
+
+```bash
+NODE_ENV=production
+```
+
+- Use the `start` command to run the application:
+
+```bash npm2yarn
+cd .medusa/server && npm run start
+```
+
+***
+
+## Build Medusa Admin
+
+By default, the Medusa Admin is built to the `.medusa/server/public/admin` directory.
+
+If you want a separate build to host the admin standalone, such as on Vercel, pass the `--admin-only` option as explained in the [Options](#options) section. This outputs the admin to the `.medusa/admin` directory instead.
+
+
+# develop Command - Medusa CLI Reference
+
+Start Medusa application in development. This command watches files for any changes, then rebuilds the files and restarts the Medusa application.
+
+```bash
+npx medusa develop
+```
+
+## Options
+
+|Option|Description|Default|
+|---|---|---|---|---|
+|\`-H \\`|Set host of the Medusa server.|\`localhost\`|
+|\`-p \\`|Set port of the Medusa server.|\`9000\`|
+
+
+# exec Command - Medusa CLI Reference
+
+Run a custom CLI script. Learn more about it in [this guide](https://docs.medusajs.com/docs/learn/fundamentals/custom-cli-scripts/index.html.md).
+
+```bash
+npx medusa exec [file] [args...]
+```
+
+## Arguments
+
+|Argument|Description|Required|
+|---|---|---|---|---|
+|\`file\`|The path to the TypeScript or JavaScript file holding the function to execute.|Yes|
+|\`args\`|A list of arguments to pass to the function. These arguments are passed in the |No|
+
+
+# new Command - Medusa CLI Reference
+
+Create a new Medusa application. Unlike the `create-medusa-app` CLI tool, this command provides more flexibility for experienced Medusa developers in creating and configuring their project.
+
+```bash
+medusa new [ []]
+```
+
+## Arguments
+
+|Argument|Description|Required|Default|
+|---|---|---|---|---|---|---|
+|\`dir\_name\`|The name of the directory to create the Medusa application in.|Yes|-|
+|\`starter\_url\`|The URL of the starter repository to create the project from.|No|\`https://github.com/medusajs/medusa-starter-default\`|
+
+## Options
+
+|Option|Description|
+|---|---|---|
+|\`-y\`|Skip all prompts, such as databaes prompts. A database might not be created if default PostgreSQL credentials don't work.|
+|\`--skip-db\`|Skip database creation.|
+|\`--skip-env\`|Skip populating |
+|\`--db-user \\`|The database user to use for database setup.|
+|\`--db-database \\`|The name of the database used for database setup.|
+|\`--db-pass \\`|The database password to use for database setup.|
+|\`--db-port \\`|The database port to use for database setup.|
+|\`--db-host \\`|The database host to use for database setup.|
+
+
+# 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.|
+
+
+# telemetry Command - Medusa CLI Reference
+
+Enable or disable the collection of anonymous data usage. If no option is provided, the command enables the collection of anonymous data usage.
+
+```bash
+npx medusa telemetry
+```
+
+#### Options
+
+|Option|Description|
+|---|---|---|
+|\`--enable\`|Enable telemetry (default).|
+|\`--disable\`|Disable telemetry.|
+
+
+# user Command - Medusa CLI Reference
+
+Create a new admin user.
+
+```bash
+npx medusa user --email [--password ]
+```
+
+## Options
+
+|Option|Description|Required|Default|
+|---|---|---|---|---|---|---|
+|\`-e \\`|The user's email.|Yes|-|
+|\`-p \\`|The user's password.|No|-|
+|\`-i \\`|The user's ID.|No|An automatically generated ID.|
+|\`--invite\`|Whether to create an invite instead of a user. When using this option, you don't need to specify a password.
+If ran successfully, you'll receive the invite token in the output.|No|\`false\`|
+
+
+# plugin Commands - Medusa CLI Reference
+
+Commands starting with `plugin:` perform actions related to [plugin](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md) development.
+
+These commands are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0).
+
+## plugin:publish
+
+Publish a plugin into the local packages registry. The command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. You can then install the plugin in a local Medusa project using the [plugin:add](#pluginadd) command.
+
+```bash
+npx medusa plugin:publish
+```
+
+***
+
+## plugin:add
+
+Install the specified plugins from the local package registry into a local Medusa application. Plugins can be added to the local package registry using the [plugin:publish](#pluginpublish) command.
+
+```bash
+npx medusa plugin:add [names...]
+```
+
+### Arguments
+
+|Argument|Description|Required|
+|---|---|---|---|---|
+|\`names\`|The names of one or more plugins to install from the local package registry. A plugin's name is as specified in its |Yes|
+
+***
+
+## plugin:develop
+
+Start a development server for a plugin. The command will watch for changes in the plugin's source code and automatically re-publish the changes into the local package registry.
+
+```bash
+npx medusa plugin:develop
+```
+
+***
+
+## plugin:db:generate
+
+Generate migrations for all modules in a plugin.
+
+```bash
+npx medusa plugin:db:generate
+```
+
+***
+
+## plugin:build
+
+Build a plugin before publishing it to NPM. The command will compile an output in the `.medusa/server` directory.
+
+```bash
+npx medusa plugin:build
+```
+
+
# db Commands - Medusa CLI Reference
Commands starting with `db:` perform actions on the database.
@@ -36477,242 +36744,6 @@ npx medusa db:sync-links
|\`--execute-all\`|Skip prompts when syncing links and execute all (including unsafe) actions.|No|Prompts are shown for unsafe actions, by default.|
-# 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.|
-
-
-# exec Command - Medusa CLI Reference
-
-Run a custom CLI script. Learn more about it in [this guide](https://docs.medusajs.com/docs/learn/fundamentals/custom-cli-scripts/index.html.md).
-
-```bash
-npx medusa exec [file] [args...]
-```
-
-## Arguments
-
-|Argument|Description|Required|
-|---|---|---|---|---|
-|\`file\`|The path to the TypeScript or JavaScript file holding the function to execute.|Yes|
-|\`args\`|A list of arguments to pass to the function. These arguments are passed in the |No|
-
-
-# develop Command - Medusa CLI Reference
-
-Start Medusa application in development. This command watches files for any changes, then rebuilds the files and restarts the Medusa application.
-
-```bash
-npx medusa develop
-```
-
-## Options
-
-|Option|Description|Default|
-|---|---|---|---|---|
-|\`-H \\`|Set host of the Medusa server.|\`localhost\`|
-|\`-p \\`|Set port of the Medusa server.|\`9000\`|
-
-
-# plugin Commands - Medusa CLI Reference
-
-Commands starting with `plugin:` perform actions related to [plugin](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md) development.
-
-These commands are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0).
-
-## plugin:publish
-
-Publish a plugin into the local packages registry. The command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. You can then install the plugin in a local Medusa project using the [plugin:add](#pluginadd) command.
-
-```bash
-npx medusa plugin:publish
-```
-
-***
-
-## plugin:add
-
-Install the specified plugins from the local package registry into a local Medusa application. Plugins can be added to the local package registry using the [plugin:publish](#pluginpublish) command.
-
-```bash
-npx medusa plugin:add [names...]
-```
-
-### Arguments
-
-|Argument|Description|Required|
-|---|---|---|---|---|
-|\`names\`|The names of one or more plugins to install from the local package registry. A plugin's name is as specified in its |Yes|
-
-***
-
-## plugin:develop
-
-Start a development server for a plugin. The command will watch for changes in the plugin's source code and automatically re-publish the changes into the local package registry.
-
-```bash
-npx medusa plugin:develop
-```
-
-***
-
-## plugin:db:generate
-
-Generate migrations for all modules in a plugin.
-
-```bash
-npx medusa plugin:db:generate
-```
-
-***
-
-## plugin:build
-
-Build a plugin before publishing it to NPM. The command will compile an output in the `.medusa/server` directory.
-
-```bash
-npx medusa plugin:build
-```
-
-
-# build Command - Medusa CLI Reference
-
-Create a standalone build of the Medusa application.
-
-This creates a build that:
-
-- Doesn't rely on the source TypeScript files.
-- Can be copied to a production server reliably.
-
-The build is outputted to a new `.medusa/server` directory.
-
-```bash
-npx medusa build
-```
-
-Refer to [this section](#run-built-medusa-application) for next steps.
-
-## Options
-
-|Option|Description|
-|---|---|---|
-|\`--admin-only\`|Whether to only build the admin to host it separately. If this option is not passed, the admin is built to the |
-
-***
-
-## Run Built Medusa Application
-
-After running the `build` command, use the following step to run the built Medusa application:
-
-- Change to the `.medusa/server` directory and install the dependencies:
-
-```bash npm2yarn
-cd .medusa/server && npm install
-```
-
-- When running the application locally, make sure to copy the `.env` file from the root project's directory. In production, use system environment variables instead.
-
-```bash npm2yarn
-cp .env .medusa/server/.env.production
-```
-
-- In the system environment variables, set `NODE_ENV` to `production`:
-
-```bash
-NODE_ENV=production
-```
-
-- Use the `start` command to run the application:
-
-```bash npm2yarn
-cd .medusa/server && npm run start
-```
-
-***
-
-## Build Medusa Admin
-
-By default, the Medusa Admin is built to the `.medusa/server/public/admin` directory.
-
-If you want a separate build to host the admin standalone, such as on Vercel, pass the `--admin-only` option as explained in the [Options](#options) section. This outputs the admin to the `.medusa/admin` directory instead.
-
-
-# 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.|
-
-
-# telemetry Command - Medusa CLI Reference
-
-Enable or disable the collection of anonymous data usage. If no option is provided, the command enables the collection of anonymous data usage.
-
-```bash
-npx medusa telemetry
-```
-
-#### Options
-
-|Option|Description|
-|---|---|---|
-|\`--enable\`|Enable telemetry (default).|
-|\`--disable\`|Disable telemetry.|
-
-
-# user Command - Medusa CLI Reference
-
-Create a new admin user.
-
-```bash
-npx medusa user --email [--password ]
-```
-
-## Options
-
-|Option|Description|Required|Default|
-|---|---|---|---|---|---|---|
-|\`-e \\`|The user's email.|Yes|-|
-|\`-p \\`|The user's password.|No|-|
-|\`-i \\`|The user's ID.|No|An automatically generated ID.|
-|\`--invite\`|Whether to create an invite instead of a user. When using this option, you don't need to specify a password.
-If ran successfully, you'll receive the invite token in the output.|No|\`false\`|
-
-
# Medusa JS SDK
In this documentation, you'll learn how to install and use Medusa's JS SDK.
@@ -45866,2055 +45897,6 @@ 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.
-# Implement Phone Authentication and Integrate Twilio SMS
-
-In this tutorial, you will learn how to implement phone number authentication in your Medusa application.
-
-When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md), which are available out-of-the-box. These features include authentication with custom providers and for custom user or actor types.
-
-In this tutorial, you'll learn how to implement a custom authentication provider that allows customers to log in with their phone number. You'll also integrate [Twilio](https://www.twilio.com/en-us/messaging/channels/sms) to send SMS messages to those customers with the one-time password (OTP) for authentication.
-
-Twilio is just one option to deliver the OTP to the customer. You can integrate a different SMS provider or use a different method to send OTPs.
-
-## Summary
-
-By following this tutorial, you will learn how to:
-
-- Install and set up Medusa.
-- Implement a custom phone authentication provider.
-- Integrate Twilio to send OTPs by SMS.
-- Customize the Next.js Starter Storefront to allow customers to log in with their phone numbers.
-
-You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer.
-
-
-
-While this tutorial focuses on supporting phone authentication for customers, you can use the authentication provider for any actor type, such as admin user or vendor. [At the end of this tutorial](#next-steps), you'll learn how to authenticate other actor types.
-
-- [Phone Authentication Repository](https://github.com/medusajs/examples/tree/main/phone-auth): Find the full code for this guide in this repository.
-- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1747745832/OpenApi/Phone_Auth_g4xsqv.yaml): Import this OpenApi Specs file into tools like Postman.
-
-***
-
-## 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: Implement Phone Authentication Module Provider
-
-In Medusa, you integrate custom authentication providers by creating an [Authentication Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/index.html.md). Then, you can use that provider to authenticate users using custom logic.
-
-In this step, you'll create a Phone Authentication Module Provider that allows users to log in with their phone numbers and an OTP. Later, you'll integrate Twilio to send the OTPs to the users, and customize the storefront to allow customers to log in with their phone numbers.
-
-An Authentication Module Provider doesn't need to handle storing and managing specific user details, such as creating customers or admin users. Instead, it only focuses on the logic of authenticating a type of user using custom logic or integration. You can learn more in the [Auth Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/index.html.md) documentation.
-
-### Prerequisite: Install jsonwebtoken
-
-In the Phone Authentication Module Provider, you'll use the `jsonwebtoken` package to sign and verify the OTPs.
-
-To install the package, run the following command in the Medusa application directory:
-
-```bash npm2yarn
-npm install jsonwebtoken
-npm install @types/jsonwebtoken --save-dev
-```
-
-### a. Create Module Directory
-
-Modules are created under the `src/modules` directory. So, start by creating the directory `src/modules/phone-auth`.
-
-### b. Create Auth Module Provider Service
-
-A module has a service that contains its logic. For Authentication Module Providers, the service implements the logic to authenticate users.
-
-To create the service of the Phone Authentication Module Provider, create the file `src/modules/phone-auth/service.ts` with the following content:
-
-```ts title="src/modules/phone-auth/service.ts" highlights={phoneAuthServiceHighlights}
-import {
- AbstractAuthModuleProvider,
- AbstractEventBusModuleService,
-} from "@medusajs/framework/utils"
-import {
- Logger,
-} from "@medusajs/types"
-
-type InjectedDependencies = {
- logger: Logger
- event_bus: AbstractEventBusModuleService
-}
-
-type Options = {
- jwtSecret: string
-}
-
-class PhoneAuthService extends AbstractAuthModuleProvider {
- static DISPLAY_NAME = "Phone Auth"
- static identifier = "phone-auth"
- private options: Options
- private logger: Logger
- private event_bus: AbstractEventBusModuleService
-
- constructor(container: InjectedDependencies, options: Options) {
- // @ts-ignore
- super(...arguments)
-
- this.options = options
- this.logger = container.logger
- this.event_bus = container.event_bus
- }
-}
-
-export default PhoneAuthService
-```
-
-An Authentication Module Provider's service must extend the `AbstractAuthModuleProvider` class. You'll get a type error about implementing the abstract methods of that class, which you'll add in the next steps.
-
-An Authentication Module Provider must also have the following static properties:
-
-- `identifier`: A unique identifier for the provider.
-- `DISPLAY_NAME`: A human-readable name for the provider. This name is used for display purposes.
-
-A module provider's constructor receives two parameters:
-
-- `container`: The [module's container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md) that contains Framework resources available to the module. You access the following resources:
- - `logger`: A [Logger](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md) class to log debug messages.
- - `event_bus`: The [Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/index.html.md)'s service to emit events.
-- `options`: Options that are passed to the module provider when it's registered in Medusa's configurations. You define the following option:
- - `jwtSecret`: A secret used to sign and verify the OTPs.
-
-You'll learn how to set this option when you [add the module provider to Medusa's configurations](#h-add-module-provider-to-medusas-configurations).
-
-In the constructor, you set the class's properties to the injected dependencies and options.
-
-In the next sections, you'll implement the methods of the `AbstractAuthModuleProvider` class.
-
-Refer to the [Create Auth Module Provider](https://docs.medusajs.com/references/auth/provider/index.html.md) guide for detailed information about the methods.
-
-### c. Implement validateOptions Method
-
-The `validateOptions` method is used to validate the options passed to the module provider. If the method throws an error, the Medusa application won't start.
-
-So, add the `validateOptions` method to the `PhoneAuthService` class:
-
-```ts title="src/modules/phone-auth/service.ts"
-// other imports...
-import {
- MedusaError,
-} from "@medusajs/framework/utils"
-
-class PhoneAuthService extends AbstractAuthModuleProvider {
- // ...
- static validateOptions(options: Record): void | never {
- if (!options.jwtSecret) {
- throw new MedusaError(
- MedusaError.Types.INVALID_DATA,
- "JWT secret is required"
- )
- }
- }
-}
-```
-
-The `validateOptions` method receives the options passed to the module provider as a parameter.
-
-In the method, you throw an error if the `jwtSecret` option is not set.
-
-### d. Implement register Method
-
-When a customer (or another actor type) registers in your application, they must also have an [auth identity](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-identity-and-actor-types/index.html.md) that allows them to login.
-
-The `register` method of an auth provider uses custom logic to create the auth identity for the actor type (such as customer). In the method, you can perform custom validation and specify the custom authentication details to store for the user's auth identity.
-
-Medusa uses the `register` method to create an auth identity that will be associated with the customer when they register. You can learn more in the [Authentication Flows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-flows/index.html.md) documentation.
-
-
-
-So, add the `register` method to the `PhoneAuthService` class:
-
-```ts title="src/modules/phone-auth/service.ts" highlights={registerHighlights}
-// other imports...
-import {
- AuthenticationInput,
- AuthIdentityProviderService,
- AuthenticationResponse,
-} from "@medusajs/types"
-
-class PhoneAuthService extends AbstractAuthModuleProvider {
- // ...
- async register(
- data: AuthenticationInput,
- authIdentityProviderService: AuthIdentityProviderService
- ): Promise {
- const { phone } = data.body || {}
-
- if (!phone) {
- return {
- success: false,
- error: "Phone number is required",
- }
- }
-
- try {
- await authIdentityProviderService.retrieve({
- entity_id: phone,
- })
-
- return {
- success: false,
- error: "User with phone number already exists",
- }
- } catch (error) {
- const user = await authIdentityProviderService.create({
- entity_id: phone,
- })
-
- return {
- success: true,
- authIdentity: user,
- }
- }
- }
-}
-```
-
-#### Parameters
-
-The `register` method receives an object parameter with the following properties:
-
-- `data`: An object containing properties like `body` that holds request-body parameters. Clients will pass relevant authentication data, such as the user's phone number, in the request body.
-- `authIdentityProviderService`: A service injected by the [Auth Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/index.html.md) that allows you to manage auth identities.
-
-The method receives other parameters, which you can find in the [Create Auth Module Provider](https://docs.medusajs.com/references/auth/provider#register/index.html.md) guide.
-
-#### Method Logic
-
-In the method, you extract the `phone` property from the request body, and return an error if it's not provided. You also return an error if another user is using the same phone number.
-
-Otherwise, you create a new auth identity for the user. You set the phone number as the `entity_id` of the auth identity, which is a unique identifier.
-
-#### Return Value
-
-Finally, you return an object with the following properties:
-
-- `success`: A boolean indicating whether the registration was successful.
-- `authIdentity`: The created auth identity of the user. This property is only set if the registration was successful.
-- `error`: An error message if the registration failed.
-
-### e. Implement authenticate Method
-
-When a customer (or another actor type) logs in, the `authenticate` method of an auth provider is called. This method uses custom logic to authenticate the user.
-
-Authentication providers may implement one of the following flows:
-
-- Direct authentication, where the user is authenticated using this method only. For example, authenticating with an email and password.
-- Authentication with callback verification, where the user is authenticated using this method and then a callback is used to verify additional information.
-
-For the Phone Authentication Module Provider, you'll implement the second flow. The user will first be authenticated using the `authenticate` method to make sure the user exists and generate an OTP. Then, they need to supply the OTP to verify their identity.
-
-
-
-So, add the `authenticate` method to the `PhoneAuthService` class:
-
-```ts title="src/modules/phone-auth/service.ts" highlights={authenticateHighlights}
-// other imports...
-import {
- AuthIdentityDTO,
-} from "@medusajs/types"
-import jwt from "jsonwebtoken"
-
-class PhoneAuthService extends AbstractAuthModuleProvider {
- // ...
- async authenticate(
- data: AuthenticationInput,
- authIdentityProviderService: AuthIdentityProviderService
- ): Promise {
- const { phone } = data.body || {}
-
- if (!phone) {
- return {
- success: false,
- error: "Phone number is required",
- }
- }
-
- try {
- await authIdentityProviderService.retrieve({
- entity_id: phone,
- })
- } catch (error) {
- return {
- success: false,
- error: "User with phone number does not exist",
- }
- }
-
- const { hashedOTP, otp } = await this.generateOTP()
-
- await authIdentityProviderService.update(phone, {
- provider_metadata: {
- otp: hashedOTP,
- },
- })
-
- await this.event_bus.emit({
- name: "phone-auth.otp.generated",
- data: {
- otp,
- phone,
- },
- }, {})
-
- return {
- success: true,
- location: "otp",
- }
- }
-
- async generateOTP(): Promise<{ hashedOTP: string, otp: string }> {
- // Generate a 6-digit OTP
- const otp = Math.floor(100000 + Math.random() * 900000).toString()
-
- // for debug
- this.logger.info(`Generated OTP: ${otp}`)
-
- const hashedOTP = jwt.sign({ otp }, this.options.jwtSecret, {
- expiresIn: "60s",
- })
-
- return { hashedOTP, otp }
- }
-}
-```
-
-You add two methods: the `authenticate` method, and a helper `generateOTP` method.
-
-#### authenticate Parameters
-
-The `authenticate` method receives an object parameter with the following properties:
-
-- `data`: An object containing properties like `body` that holds request-body parameters. Clients will pass relevant authentication data, such as the user's phone number, in the request body.
-- `authIdentityProviderService`: A service injected by the [Auth Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/index.html.md) that allows you to manage auth identities.
-
-The method receives other parameters, which you can find in the [Create Auth Module Provider](https://docs.medusajs.com/references/auth/provider#authenticate/index.html.md) guide.
-
-#### authenticate Logic
-
-In the method, you return an error if the `phone` property is not provided in the request body, or if a user with that phone number doesn't exist.
-
-Next, you generate a 6-digit OTP using the `generateOTP` method. Notice that you currently log the OTP for debugging purposes. You can remove this line later once you integrate Twilio.
-
-The OTP is hashed and stored in the `provider_metadata` property of the user's auth identity. The `provider_metadata` property is a JSON object that stores additional information about the auth identity.
-
-Then, you emit an event with the generated OTP and the user's phone number. This allows you later to handle the event and send the OTP to the user using services like Twilio.
-
-#### authenticate Return Value
-
-Finally, you return an object with the following properties:
-
-- `success`: A boolean indicating whether the authentication was successful.
-- `location`: A string indicating a URL to perform additional actions in. In this case, you set the location to `otp`, indicating that the user should verify with the OTP.
-- `error`: An error message if the authentication failed.
-
-### f. Implement validateCallback Method
-
-When an authentication provider requires a callback to verify the user, the Medusa application calls the `validateCallback` method.
-
-You can use this method to verify the OTP that the user entered. If valid, you return the logged in user, and the Medusa application will return a JWT token that the user can use to authenticate in the application.
-
-
-
-So, add the `validateCallback` method to the `PhoneAuthService` class:
-
-```ts title="src/modules/phone-auth/service.ts" highlights={validateCallbackHighlights}
-class PhoneAuthService extends AbstractAuthModuleProvider {
- // ...
- async validateCallback(
- data: AuthenticationInput,
- authIdentityProviderService: AuthIdentityProviderService
- ): Promise {
- const { phone, otp } = data.query || {}
-
- if (!phone || !otp) {
- return {
- success: false,
- error: "Phone number and OTP are required",
- }
- }
-
- const user = await authIdentityProviderService.retrieve({
- entity_id: phone,
- })
-
- if (!user) {
- return {
- success: false,
- error: "User with phone number does not exist",
- }
- }
-
- // verify that OTP is correct
- const userProvider = user.provider_identities?.find((provider) => provider.provider === this.identifier)
- if (!userProvider || !userProvider.provider_metadata?.otp) {
- return {
- success: false,
- error: "User with phone number does not have a phone auth provider",
- }
- }
-
- try {
- const decodedOTP = jwt.verify(
- userProvider.provider_metadata.otp as string,
- this.options.jwtSecret
- ) as { otp: string }
-
- if (decodedOTP.otp !== otp) {
- throw new Error("Invalid OTP")
- }
- } catch (error) {
- return {
- success: false,
- error: error.message || "Invalid OTP",
- }
- }
-
- const updatedUser = await authIdentityProviderService.update(phone, {
- provider_metadata: {
- otp: null,
- },
- })
-
- return {
- success: true,
- authIdentity: updatedUser,
- }
- }
-}
-```
-
-#### Parameters
-
-The `validateCallback` method receives an object parameter with the following properties:
-
-- `data`: An object containing properties like `query` that holds query parameters. Clients will pass relevant authentication data, such as the user's phone number and OTP, in the request query.
-- `authIdentityProviderService`: A service injected by the [Auth Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/index.html.md) that allows you to manage auth identities.
-
-The method receives other parameters, which you can find in the [Create Auth Module Provider](https://docs.medusajs.com/references/auth/provider#validatecallback/index.html.md) guide.
-
-#### Method Logic
-
-In the method, you return an error if the phone and otp aren't provided in the request query, or if a user with that phone number doesn't exist.
-
-Next, you verify that the OTP provided by the user is correct. You retrieve the hashed OTP from the `provider_metadata` property of the user's auth identity. If the OTP is not valid, you return an error.
-
-Since you set the hash expiration to 60 seconds, the OTP will be valid for 60 seconds. After that, the user will need to request a new OTP.
-
-After that, you update the user's auth identity to remove the OTP from the `provider_metadata` property.
-
-#### Return Value
-
-Finally, you return an object with the following properties:
-
-- `success`: A boolean indicating whether the authentication was successful.
-- `authIdentity`: The user's auth identity. This property is only set if the authentication was successful.
-- `error`: An error message if the authentication failed.
-
-### g. Export Module Definition
-
-You've now finished implementing the necessary methods for the Phone Authentication Module Provider.
-
-The final piece to a module is its definition, which you export in an `index.ts` file at the module's root directory. This definition tells Medusa the name of the module, its service, and optionally its loaders.
-
-To create the module's definition, create the file `src/modules/phone-auth/index.ts` with the following content:
-
-```ts title="src/modules/phone-auth/index.ts"
-import PhoneAuthService from "./service"
-import {
- ModuleProvider,
- Modules,
-} from "@medusajs/framework/utils"
-
-export default ModuleProvider(Modules.AUTH, {
- services: [PhoneAuthService],
-})
-```
-
-You use `ModuleProvider` from the Modules SDK to create the module provider's definition. It accepts two parameters:
-
-1. The name of the module that this provider belongs to, which is `Modules.AUTH` in this case.
-2. An object with a required property `services` indicating the module provider's services. Each of these services will be registered as authentication providers in Medusa.
-
-### h. Add Module Provider to Medusa's Configurations
-
-Once you finish building the module, add it to Medusa's configurations to start using it.
-
-In `medusa-config.ts`, add a `modules` property:
-
-```ts title="medusa-config.ts"
-// other imports...
-import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils"
-
-module.exports = defineConfig({
- // ...
- modules: [
- {
- resolve: "@medusajs/medusa/auth",
- dependencies: [
- Modules.CACHE,
- ContainerRegistrationKeys.LOGGER,
- Modules.EVENT_BUS,
- ],
- options: {
- providers: [
- // default provider
- {
- resolve: "@medusajs/medusa/auth-emailpass",
- id: "emailpass",
- },
- {
- resolve: "./src/modules/phone-auth",
- id: "phone-auth",
- options: {
- jwtSecret: process.env.PHONE_AUTH_JWT_SECRET || "supersecret",
- },
- },
- ],
- },
- },
- ],
-})
-```
-
-To pass an Auth Module Provider to the Auth Module, you add the `modules` property to the Medusa configuration and pass the Auth Module in its value.
-
-The Auth Module accepts a `dependencies` option, allowing you to inject dependencies into the containers of the module and its providers. The Auth Module requires passing the [Cache Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/index.html.md) and Logger, but you also inject the `event_bus` dependency to use the Event Module's service in the Phone Authentication Module Provider.
-
-The Auth Module also accepts a `providers` option, which is an array of Auth Module Providers to register. You register the `emailpass` provider, which is registered by default when you don't provide any other providers.
-
-To register the Phone Authentication Module Provider, you add an object to the `providers` array with the following properties:
-
-- `resolve`: The NPM package or path to the module provider. In this case, it's the path to the `src/modules/phone-auth` directory.
-- `id`: The ID of the module provider. The auth provider is then registered with the ID `au_{id}`.
-- `options`: The options to pass to the module provider. These are the options you defined in the `Options` interface of the module provider's service.
-
-### i. Enable Phone Authentication for Customers
-
-By default, customers and admin users can be authenticated using the `emailpass` provider. When you add a new provider, you need to specify which actor types can use it.
-
-In `medusa-config.ts`, add to `projectConfig.http` a new `authMethodsPerActor` property:
-
-```ts title="medusa-config.ts" highlights={authMethodsPerActorHighlights}
-module.exports = defineConfig({
- projectConfig: {
- // ...
- http: {
- // ...
- authMethodsPerActor: {
- user: ["emailpass"],
- customer: ["emailpass", "phone-auth"],
- },
- },
- },
- // ...
-})
-```
-
-The `authMethodsPerActor` property is an object whose keys are actor types. The values are arrays of authentication method IDs that can be used for that actor type.
-
-In this case, you enable the `phone-auth` provider for customers. You can also enable it for other actor types, such as admin users or vendors.
-
-### Test Out Phone Authentication
-
-In this section, you'll test out the Phone Authentication Module Provider using Medusa's API routes. You can, instead, test it out later using the [Next.js Starter Storefront](#step-4-use-phone-authentication-in-the-nextjs-starter-storefront).
-
-First, start the Medusa application with the following command:
-
-```bash npm2yarn
-npm run start
-```
-
-#### Prerequisite: Retrieve Publishable API Key
-
-Before you start testing the authentication provider using the API routes, you need to retrieve your application's publishable API key. This key is necessary to send requests to API routes starting with `/store`.
-
-To retrieve the publishable API key:
-
-1. Open the Medusa Admin dashboard at `http://localhost:9000/admin` and log in.
-2. Go to Settings -> Publishable API Keys.
-3. Click on the API key in the table.
-4. In its details page, click on the API key to copy it.
-
-
-
-#### a. Retrieve Registration Token
-
-The first step is to retrieve a registration token for a new customer. This token will allow them to register in the application.
-
-To retrieve the registration token, send a `POST` request to `/auth/customer/phone-auth/register`:
-
-```bash
-curl -X POST 'http://localhost:9000/auth/customer/phone-auth/register' \
---header 'Content-Type: application/json' \
---data '{
- "phone": "+19077890116"
-}'
-```
-
-Make sure to replace the phone number with the one you want to use.
-
-This will return a `token` in the response:
-
-```json title="Example Response"
-{
- "token": "123..."
-}
-```
-
-#### b. Register Customer
-
-Next, you'll register the customer using the [Register Customer](https://docs.medusajs.com/api/store#customers_postcustomers) API route. You'll pass the registration token you received in the previous step in the header of this request.
-
-So, send a `POST` request to `/store/customers`:
-
-```bash
-curl -X POST 'http://localhost:9000/store/customers' \
---header 'x-publishable-api-key: {publishable_api_key}' \
---header 'Content-Type: application/json' \
---header 'Authorization: Bearer {reg_token}' \
---data-raw '{
- "email": "+19077890116@gmail.com",
- "phone": "19077890116",
- "first_name": "John",
- "last_name": "Smith"
-}'
-```
-
-Make sure to replace:
-
-- `{publishable_api_key}` with the publishable API key you retrieved from the Medusa Admin dashboard.
-- `{reg_token}` with the registration token you received in the previous step.
-- The customer details in the request body with the ones you want to use. Use the same phone number you used in the previous step.
- - You pass the email because it's required by the [Register Customer](https://docs.medusajs.com/api/store#customers_postcustomers) API route. You set it to the phone number with a `gmail.com` domain.
-
-The request will return the created customer's details:
-
-```json title="Example Response"
-{
- "customer": {
- "id": "cus_01JVPESW5SM1MSVPNM2MSC0ZEC",
- "email": "+19077890116@gmail.com",
- "company_name": null,
- "first_name": "John",
- "last_name": "Smith",
- "phone": "19077890116",
- "metadata": null,
- "has_account": true,
- "deleted_at": null,
- "created_at": "2025-05-20T09:01:13.273Z",
- "updated_at": "2025-05-20T09:01:13.273Z",
- "addresses": []
- }
-}
-```
-
-The customer can now authenticate using the phone number and OTP.
-
-#### c. Authenticate Customer
-
-Next, you'll authenticate the customer using the [Authenticate Customer](https://docs.medusajs.com/api/store#auth_postactor_typeauth_provider) API route. This would send the customer an OTP to their phone number (which you'll implement in the next step).
-
-So, send a `POST` request to `/auth/customer/phone-auth`:
-
-```bash
-curl -X POST 'http://localhost:9000/auth/customer/phone-auth' \
---header 'Content-Type: application/json' \
---data '{
- "phone": "+19077890116"
-}'
-```
-
-Make sure to replace the phone number with the one you used to register the customer.
-
-This will return a `location` in the response:
-
-```json title="Example Response"
-{
- "location": "otp"
-}
-```
-
-Indicating that the user should verify their OTP.
-
-You can also use this route to resend the OTP if the user didn't receive it or if a minute has passed since the last OTP was sent.
-
-#### d. Verify OTP
-
-If you check the logs of the Medusa application, you'll see that the OTP was generated and logged:
-
-```bash
-info: Generated OTP: 576794
-```
-
-As mentioned before, this is only for debugging purposes. In the next step, you'll implement the logic to send the OTP to the user using Twilio.
-
-So, to verify the OTP, you'll send a request to the [Verify Callback API route](https://docs.medusajs.com/api/store#auth_postactor_typeauth_providercallback):
-
-```bash
-curl -X POST 'http://localhost:9000/auth/customer/phone-auth/callback?phone=%2B19077890116&otp=476588'
-```
-
-You pass the following query parameters:
-
-- `phone`: The phone number of the customer. Make sure to use the same phone number you used to register the customer, and to encode it. For example, the `+` sign should be encoded as `%2B`.
-- `otp`: The OTP that the customer received. Make sure to use the same OTP shown in the logs.
-
-If the OTP is valid, you'll receive a JWT token in the response:
-
-```json title="Example Response"
-{
- "token": "123..."
-}
-```
-
-You can use this token to authenticate the customer in the application. For example, you can use the token to [retrieve the customer's details](https://docs.medusajs.com/api/store#customers_getcustomersme).
-
-If the OTP has expired, send a request to the [Authenticate Customer](#c-authenticate-customer) API route to generate a new OTP
-
-***
-
-## Step 3: Integrate Twilio SMS
-
-Similar to the Auth Module, the [Notification Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/index.html.md) allows registering custom providers to send notifications, such as SMS or email.
-
-In this step, you'll create a Twilio Notification Module Provider, then use it to send the OTP to the customer.
-
-### Prerequisites
-
-- [Twilio Account](https://console.twilio.com/)
-- [Twilio From Phone Number](https://www.twilio.com/docs/phone-numbers)
-- [Twilio Account SID, which you can retrieve from the Twilio Console homepage.](https://www.twilio.com/docs/usage/tutorials/how-to-use-your-free-trial-account-namer#console-dashboard-home-page)
-- [Twilio Auth Token, which you can retrieve from the Twilio Console homepage.](https://www.twilio.com/docs/usage/tutorials/how-to-use-your-free-trial-account-namer#console-dashboard-home-page)
-
-### a. Install Twilio SDK
-
-Before you start implementing the Twilio Notification Module Provider, install the Twilio SDK to interact with the Twilio API.
-
-Run the following command in the Medusa application directory:
-
-```bash npm2yarn
-npm install twilio
-```
-
-You'll use the Twilio SDK in the Notification Module Provider's service.
-
-### b. Create Module Directory
-
-Create the directory `src/modules/twilio-sms` to create the Twilio Notification Module Provider.
-
-### c. Create Notification Module Provider Service
-
-A Notification Module Provider has a service that contains the sending logic. The service must extend the `AbstractNotificationProviderService` class.
-
-So, create the file `src/modules/twilio-sms/service.ts` with the following content:
-
-```ts title="src/modules/twilio-sms/service.ts" highlights={twilioSmsServiceHighlights}
-import {
- AbstractNotificationProviderService,
-} from "@medusajs/framework/utils"
-import { Twilio } from "twilio"
-
-type InjectedDependencies = {}
-
-type TwilioSmsServiceOptions = {
- accountSid: string
- authToken: string
- from: string
-}
-
-class TwilioSmsService extends AbstractNotificationProviderService {
- static readonly identifier = "twilio-sms"
- private readonly client: Twilio
- private readonly from: string
-
- constructor(container: InjectedDependencies, options: TwilioSmsServiceOptions) {
- super()
-
- this.client = new Twilio(options.accountSid, options.authToken)
- this.from = options.from
- }
-}
-```
-
-You'll get a type error about implementing the abstract methods of the `AbstractNotificationProviderService` class, which you'll add in the next steps.
-
-A Notification Module Provider must have an `identifier` static property, which is a unique identifier for the module. This identifier is used to register the module in the Medusa application.
-
-A module provider's constructor receives two parameters:
-
-- `container`: The [module's container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md) that contains Framework resources available to the module. You don't need to access any resources for this provider.
-- `options`: Options that are passed to the module provider when it's registered in Medusa's configurations. You define the following option:
- - `accountSid`: The Twilio account SID.
- - `authToken`: The Twilio auth token.
- - `from`: The Twilio phone number to send the SMS from.
-
-You'll learn how to set these options when you [add the module provider to Medusa's configurations](#g-add-module-provider-to-medusas-configurations).
-
-In the constructor, you set the class's properties to the injected dependencies and options.
-
-In the next sections, you'll implement the methods of the `AbstractNotificationProviderService` class.
-
-Refer to the [Create Notification Module Provider](https://docs.medusajs.com/references/notification-provider-module/index.html.md) guide for detailed information about the methods.
-
-### d. Implement validateOptions Method
-
-The `validateOptions` method is used to validate the options passed to the module provider. If the method throws an error, the Medusa application won't start.
-
-So, add the `validateOptions` method to the `TwilioSmsService` class:
-
-```ts title="src/modules/twilio-sms/service.ts"
-class TwilioSmsService extends AbstractNotificationProviderService {
- // ...
- static validateOptions(options: Record): void | never {
- if (!options.accountSid) {
- throw new Error("Account SID is required")
- }
- if (!options.authToken) {
- throw new Error("Auth token is required")
- }
- if (!options.from) {
- throw new Error("From is required")
- }
- }
-}
-```
-
-The `validateOptions` method receives the options passed to the module provider as a parameter.
-
-In the method, you throw an error if any of the options are not set.
-
-### e. Implement send Method
-
-The only required method for a Notification Module Provider is the `send` method. When the Medusa application needs to send a notification using the provider's channel (such as SMS), it calls this method of the registered provider.
-
-So, add the `send` method to the `TwilioSmsService` class:
-
-```ts title="src/modules/twilio-sms/service.ts" highlights={sendHighlights}
-// other imports...
-import {
- ProviderSendNotificationDTO,
- ProviderSendNotificationResultsDTO,
-} from "@medusajs/types"
-
-class TwilioSmsService extends AbstractNotificationProviderService {
- // ...
- async send(
- notification: ProviderSendNotificationDTO
- ): Promise {
- const { to, content, template, data } = notification
- const contentText = content?.text || await this.getTemplateContent(
- template, data
- )
-
- const message = await this.client.messages.create({
- body: contentText,
- from: this.from,
- to,
- })
-
- return {
- id: message.sid,
- }
- }
-
- async getTemplateContent(
- template: string,
- data?: Record | null
- ): Promise {
- switch (template) {
- case "otp-template":
- if (!data?.otp) {
- throw new Error("OTP is required for OTP template")
- }
-
- return `Your OTP is ${data.otp}`
- default:
- throw new Error(`Template ${template} not found`)
- }
- }
-}
-```
-
-You implement the `send` method and a helper `getTemplateContent` method.
-
-#### send Parameters
-
-The `send` method receives an object parameter with the following properties:
-
-- `to`: The phone number to send the SMS to.
-- `content`: An object containing the content of the SMS. The `text` property is the text to send.
-- `template`: The template to use for the SMS. This is used to retrieve the fallback content of the SMS if `content.text` is not provided.
-- `data`: An object containing the data to use in the template. This is used to replace placeholders in the template with actual values.
-
-The method receives other parameters, which you can find in the [Create Notification Module Provider](https://docs.medusajs.com/references/notification-provider-module#send/index.html.md) guide.
-
-#### send Method Logic
-
-In the method, you set the SMS content either to the `text` property of the `content` object or to the template content. You define a `getTemplateContent` method that retrieves the content for a template.
-
-Then, you use the `messages.create` method of the Twilio client to send the SMS. You pass the following parameters:
-
-- `body`: The content of the SMS.
-- `from`: The Twilio phone number to send the SMS from.
-- `to`: The phone number to send the SMS to.
-
-#### send Return Value
-
-Finally, you return an object that has an `id` property with the ID of the sent SMS. This ID is stored in the notification record in the database.
-
-### f. Export Module Definition
-
-You've now finished implementing the necessary methods for the Twilio Notification Module Provider. You only need to export its definition.
-
-To create the module's definition, create the file `src/modules/twilio-sms/index.ts` with the following content:
-
-```ts title="src/modules/twilio-sms/index.ts"
-import {
- ModuleProvider,
- Modules,
-} from "@medusajs/framework/utils"
-import TwilioSMSNotificationService from "./service"
-
-export default ModuleProvider(Modules.NOTIFICATION, {
- services: [TwilioSMSNotificationService],
-})
-```
-
-You use `ModuleProvider` from the Modules SDK to create the module provider's definition passing it two parameters:
-
-1. The name of the module that this provider belongs to, which is `Modules.NOTIFICATION` in this case.
-2. An object with a required property `services` indicating the module provider's services. Each of these services will be registered as notification providers in Medusa.
-
-### g. Add Module Provider to Medusa's Configurations
-
-You'll now add the Twilio Notification Module Provider to Medusa's configurations to start using it.
-
-In `medusa-config.ts`, add the following to the `modules` property:
-
-```ts title="medusa-config.ts"
-module.exports = defineConfig({
- // ...
- modules: [
- // ...
- {
- resolve: "@medusajs/medusa/notification",
- options: {
- providers: [
- // default provider
- {
- resolve: "@medusajs/medusa/notification-local",
- id: "local",
- options: {
- name: "Local Notification Provider",
- channels: ["feed"],
- },
- },
- {
- resolve: "./src/modules/twilio-sms",
- id: "twilio-sms",
- options: {
- channels: ["sms"],
- accountSid: process.env.TWILIO_ACCOUNT_SID,
- authToken: process.env.TWILIO_AUTH_TOKEN,
- from: process.env.TWILIO_FROM,
- },
- },
- ],
- },
- },
- ],
-})
-```
-
-You pass the Notification Module in the `modules` property to register the Twilio Notification Module Provider.
-
-The Notification Module accepts a `providers` option, which is an array of Notification Module Providers to register. You register the `local` provider, which is registered by default when you don't provide any other providers.
-
-To register the Twilio Notification Module Provider, you add an object with the following properties:
-
-- `resolve`: The path to the module provider.
-- `id`: The ID of the module provider. The notification provider is then registered with the ID `np_{identifier}_{id}`.
-- `options`: The options to pass to the module provider. These include the options you defined in the `Options` interface of the module provider's service.
- - `channels`: The channels that the notification provider supports. In this case, you set it to `sms`, which is the channel used to send SMS notifications.
- - `accountSid`: The Twilio account SID.
- - `authToken`: The Twilio auth token.
- - `from`: The Twilio phone number to send the SMS from.
-
-### h. Add Environment Variables
-
-To set the value of the Twilio options, add the following environment variables to your `.env` file:
-
-```shell title=".env"
-TWILIO_ACCOUNT_SID=AC...
-TWILIO_AUTH_TOKEN=05...
-TWILIO_FROM=+1...
-```
-
-Where:
-
-- `TWILIO_ACCOUNT_SID`: The Twilio account SID.
-- `TWILIO_AUTH_TOKEN`: The Twilio auth token.
-- `TWILIO_FROM`: The Twilio phone number to send the SMS from. Make sure to use the phone number you purchased from Twilio.
-
-You can retrieve these information from the Twilio Console homepage.
-
-
-
-### i. Handle OTP Generated Event
-
-Now that you have integrated Twilio into Medusa, you can use it to send the OTP to the customer. To do that, you need to handle the `phone-auth.otp.generated` event that you emitted in the `authenticate` method of the Phone Authentication Module Provider.
-
-You can listen to events in a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md). A subscriber is an asynchronous function that listens to events to perform actions when the event is emitted.
-
-In this step, you'll create a subscriber that listens to the `phone-auth.otp.generated` event and sends an SMS to the customer with the OTP.
-
-Refer to the [Events and Subscribers](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) documentation to learn more.
-
-Subscribers are created in a TypeScript or JavaScript file under the `src/subscribers` directory. So, to create a subscriber, create the file `src/subscribers/send-otp.ts` with the following content:
-
-```ts title="src/subscribers/send-otp.ts" highlights={sendOtpHighlights}
-import {
- SubscriberArgs,
- type SubscriberConfig,
-} from "@medusajs/medusa"
-import { Modules } from "@medusajs/framework/utils"
-
-export default async function sendOtpHandler({
- event: { data: {
- phone,
- otp,
- } },
- container,
-}: SubscriberArgs<{ phone: string, otp: string }>) {
- const notificationModuleService = container.resolve(
- Modules.NOTIFICATION
- )
-
- await notificationModuleService.createNotifications({
- to: phone,
- channel: "sms",
- template: "otp-template",
- data: {
- otp,
- },
- })
-}
-
-export const config: SubscriberConfig = {
- event: "phone-auth.otp.generated",
-}
-```
-
-The subscriber file must export:
-
-- An asynchronous subscriber function that's executed whenever the associated event is triggered.
-- A configuration object with an event property whose value is the event the subscriber is listening to, which is `phone-auth.otp.generated`.
-
-The subscriber function accepts an object with the following properties:
-
-- `event`: An object with the event's data payload. In the `authenticate` method, you emitted the event with the following data:
- - `phone`: The phone number of the user.
- - `otp`: The OTP that was generated.
-- `container`: The [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which you can use to resolve Framework and commerce resources.
-
-In the subscriber function, you resolve the Notification Module's service from the Medusa container. Then, you use its `createNotifications` method to send the OTP to the user.
-
-Under the hood, the Notification Module's service delegates the sending to the Notification Module Provider of the `sms` channel, which is the Twilio Notification Module Provider in this case.
-
-The `createNotifications` method accepts an object with the following properties:
-
-- `to`: The phone number to send the notification to. You use the phone number from the event's data payload.
-- `channel`: The channel to use to send the notification, which is `sms`.
-- `template`: The template to use for the notification content, which is `otp-template`.
-- `data`: An object containing the data to use in the template. You pass the OTP to the template.
-
-### j. Test it Out
-
-To test out the Twilio Notification Module Provider, you can follow the steps in the [Test Out Phone Authentication](#test-out-phone-authentication) section.
-
-After you authenticate the customer, the OTP will be sent to the customer's phone number using Twilio. Then, you can use the OTP to verify the authentication and receive a JWT token.
-
-Alternatively, you can also test it out after customizing the Next.js Starter Storefront, which you'll do in the next step.
-
-Make sure to remove the OTP logging line in the `generateOTP` method of the Phone Authentication Module Provider's service now that you have integrated Twilio.
-
-***
-
-## Step 4: Use Phone Authentication in the Next.js Starter Storefront
-
-In this step, you'll customize the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) to allow customers to authenticate using their phone number and OTP.
-
-By default, the Next.js Starter Storefront supports email and password authentication. You'll replace it with phone authentication, but you can also keep both authentication methods if you want to.
-
-The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.
-
-So, if your Medusa application's directory is `medusa-phone-auth`, you can find the storefront by going back to the parent directory and changing to the `medusa-phone-auth-storefront` directory:
-
-```bash
-cd ../medusa-phone-auth-storefront # change based on your project name
-```
-
-### a. Install Phone Input Package
-
-To easily show a phone input where the user can enter their phone number, install the `react-phone-number-input` package:
-
-```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
-npm install react-phone-number-input
-```
-
-You'll use it in the login and registration forms to show a phone input.
-
-### b. Add Authenticate Function
-
-Before adding the forms, you'll add the functions that send requests to the Medusa API to authenticate the customer.
-
-The first one you'll add is the `authenticateWithPhone` function, which sends a request to the `/auth/customer/phone-auth` API route to authenticate the customer using their phone number.
-
-In `src/lib/data/customer.ts`, add the following function:
-
-```ts title="src/lib/data/customer.ts" badgeLabel="Storefront" badgeColor="blue"
-export const authenticateWithPhone = async (phone: string) => {
- try {
- const response = await sdk.auth.login("customer", "phone-auth", {
- phone,
- })
-
- if (
- typeof response === "string" ||
- !response.location ||
- response.location !== "otp"
- ) {
- throw new Error("Failed to login")
- }
-
- return true
- } catch (error: any) {
- return error.toString()
- }
-}
-```
-
-The function accepts the phone number as a parameter.
-
-In the function, you use the [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md), which is configured within the Next.js Starter Storefront, to send a request to the `/auth/customer/phone-auth` API route. You pass the phone number in the request body.
-
-If the request doesn't return a `location` property set to `otp`, you throw an error. Otherwise, you return `true` to indicate that the request was successful.
-
-### c. add Verify OTP Function
-
-Next, you'll add the `verifyOTP` function, which sends a request to the `/auth/customer/phone-auth/callback` API route to verify the OTP.
-
-In `src/lib/data/customer.ts`, add the following function:
-
-```ts title="src/lib/data/customer.ts" badgeLabel="Storefront" badgeColor="blue" highlights={verifyOtpHighlights}
-export const verifyOtp = async ({
- otp,
- phone,
-}: {
- otp: string
- phone: string
-}) => {
- try {
- const token = await sdk.auth.callback("customer", "phone-auth", {
- phone,
- otp,
- })
-
- await setAuthToken(token)
-
- const customerCacheTag = await getCacheTag("customers")
- revalidateTag(customerCacheTag)
-
- await transferCart()
-
- return true
- } catch (e: any) {
- return e.toString()
- }
-}
-```
-
-The function accepts an object with the following properties:
-
-- `otp`: The OTP to verify.
-- `phone`: The phone number of the customer.
-
-In the function, you use the JS SDK to send a request to the `/auth/customer/phone-auth/callback` API route. You pass the phone number and OTP in the request body.
-
-If the request is successful and you receive a token, you set the token in the cookies and refresh the customer cache. This ensures that all customer-related UI is updated after logging in, such as showing the customer's profile when accessing the `/account` page.
-
-Then, you call the `transferCart` function to transfer the cart from the guest user to the authenticated customer.
-
-Finally, you return `true` to indicate that the request was successful.
-
-### d. Add Registration Function
-
-The last function you'll add is the `registerWithPhone` function, which will register the customer using their phone number.
-
-In `src/lib/data/customer.ts`, add the following function:
-
-```ts title="src/lib/data/customer.ts" badgeLabel="Storefront" badgeColor="blue" highlights={registerWithPhoneHighlights}
-export const registerWithPhone = async ({
- firstName,
- lastName,
- phone,
-}: {
- firstName: string
- lastName: string
- phone: string
-}) => {
- try {
- const { token: regToken } = await sdk.client.fetch<
- { token: string }
- >(`/auth/customer/phone-auth/register`, {
- method: "POST",
- body: {
- phone,
- },
- })
-
- await setAuthToken(regToken as string)
- const headers = {
- ...(await getAuthHeaders()),
- }
-
- const email = `${phone}@gmail.com`
- const customerData = {
- email,
- first_name: firstName,
- last_name: lastName,
- phone,
- }
-
- await sdk.store.customer.create(
- customerData,
- {},
- headers
- )
-
- return await authenticateWithPhone(phone)
- } catch (error: any) {
- return error.toString()
- }
-}
-```
-
-The function accepts an object with the following properties:
-
-- `firstName`: The first name of the customer.
-- `lastName`: The last name of the customer.
-- `phone`: The phone number of the customer.
-
-In the function, you retrieve a registration token for the customer using the `/auth/customer/phone-auth/register` API route. You pass the phone number in the request body.
-
-Then, after setting the registration token in the cookies, you create a customer using the [Create Customer](https://docs.medusajs.com/api/store#customers_postcustomers) API route. You pass the following properties in the request body:
-
-- `email`: The email of the customer. You set it to the phone number with a `@gmail.com` domain.
-- `first_name`: The first name of the customer.
-- `last_name`: The last name of the customer.
-- `phone`: The phone number of the customer.
-
-Finally, you call the `authenticateWithPhone` function to authenticate the customer using their phone number. At this step, the customer would receive an OTP to login.
-
-### e. Add OTP Form
-
-Next, you'll add an OTP form that allows the customer to enter the OTP they receive after login or registration. Later, you'll reuse this form in both the login and registration pages.
-
-Create the file `src/modules/account/components/otp/index.tsx` with the following content:
-
-```tsx title="src/modules/account/components/otp/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={otpHighlights}
-"use client"
-
-import { Input } from "@medusajs/ui"
-import { useState, useRef, useEffect } from "react"
-import { authenticateWithPhone, verifyOtp } from "../../../../lib/data/customer"
-import ErrorMessage from "../../../checkout/components/error-message"
-
-type Props = {
- phone: string
-}
-
-export const Otp = ({ phone }: Props) => {
- const [otp, setOtp] = useState("")
- const [error, setError] = useState("")
- const [isLoading, setIsLoading] = useState(false)
- const [countdown, setCountdown] = useState(60)
- const inputRefs = useRef<(HTMLInputElement | null)[]>([])
-
- const handleSubmit = async () => {
- setIsLoading(true)
- const response = await verifyOtp({
- otp,
- phone,
- })
- setOtp("")
- setIsLoading(false)
-
- if (typeof response === "string") {
- setError(response)
- }
- }
-
- const handleResend = async () => {
- authenticateWithPhone(phone)
- setCountdown(60)
- }
-
- const handlePaste = (e: React.ClipboardEvent) => {
- e.preventDefault()
- const pastedData = e.clipboardData.getData("text")
- const numericValue = pastedData.replace(/\D/g, "").slice(0, 6)
-
- if (numericValue) {
- setOtp(numericValue)
- // Focus the next empty input after pasted content
- const nextEmptyIndex = Math.min(numericValue.length, 5)
- inputRefs.current[nextEmptyIndex]?.focus()
- }
- }
-
- // TODO add use effects
-}
-```
-
-You create an `Otp` component that accepts the phone number as a prop.
-
-In the component, you define the following state variables:
-
-- `otp`: The OTP entered by the customer.
-- `error`: The error message to show if the OTP verification fails.
-- `isLoading`: A boolean indicating whether the OTP verification is in progress.
-- `countdown`: The countdown timer for resending the OTP.
-- `inputRefs`: A ref to store the input elements for the OTP digits. You'll show six input elements for the OTP digits.
-
-You also define the following functions:
-
-- `handleSubmit`: This function is called when the customer submits the OTP. It calls the `verifyOtp` function to verify the OTP entered by the customer.
-- `handleResend`: This function is called when the customer clicks the "Resend OTP" button that you'll add later. It calls the `authenticateWithPhone` function to resend the OTP to the customer's phone number.
-- `handlePaste`: This function is called when the customer pastes the OTP in the input field. It improves the experience of pasting the OTP without having to enter it manually.
-
-#### Handle Variable Changes
-
-Next, you'll add `useEffect` hooks to handle changes in the state variables.
-
-Replace the `TODO` with the following:
-
-```tsx title="src/modules/account/components/otp/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={useEffectHighlights}
-useEffect(() => {
- if (inputRefs.current[0]) {
- inputRefs.current[0].focus()
- }
-}, [inputRefs.current])
-
-useEffect(() => {
- if (otp.length !== 6 || isLoading) {
- return
- }
-
- handleSubmit()
-}, [otp, isLoading])
-
-useEffect(() => {
- const timer = setInterval(() => {
- setCountdown((prev) => {
- return prev > 0 ? prev - 1 : 0
- })
- }, 1000)
-
- return () => clearInterval(timer)
-}, [])
-
-// TODO render form
-```
-
-You add three `useEffect` hooks:
-
-1. The first one focuses the first input element when the component mounts.
-2. The second one automatically submits the OTP when the customer enters six digits.
-3. The third one adds an interval to update the countdown timer every second.
-
-### f. Render OTP Form
-
-Lastly, you'll render the OTP form with the input elements and the resend button.
-
-Replace the `TODO` with the following:
-
-```tsx title="src/modules/account/components/otp/index.tsx" badgeLabel="Storefront" badgeColor="blue"
-return (
-
-
- Verify Phone Number
-
-
- Enter the code sent to your phone number to login.
-
-)
-```
-
-You show six input elements for the OTP digits. When a value is entered in an input, the focus moves to the next input. In addition, when the customer presses backspace on an empty input, the focus moves to the previous input.
-
-You also show a resend button that allows the customer to resend the OTP once the countdown timer reaches zero.
-
-You now have an OTP form that you can use in both the login and registration pages.
-
-### g. Add Registration Form
-
-You'll now add a registration form that allows the customer to register using their phone number.
-
-First, in `src/modules/account/templates/login-template.tsx`, update the `LOGIN_VIEW` to the following:
-
-```tsx title="src/modules/account/templates/login-template.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["4"], ["5"]]}
-export enum LOGIN_VIEW {
- SIGN_IN = "sign-in",
- REGISTER = "register",
- REGISTER_PHONE = "register-phone",
- SIGN_IN_PHONE = "sign-in-phone",
-}
-```
-
-By default, the login template supports switching between the login and registration views for email and password authentication. With the above change, you add two new views: login and registration with phone number.
-
-Then, to add the registration form, create the file `src/modules/account/components/register-phone/index.tsx` with the following content:
-
-```tsx title="src/modules/account/components/register-phone/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={registerPhoneHighlights}
-"use client"
-
-import { useState } from "react"
-import Input from "@modules/common/components/input"
-import { LOGIN_VIEW } from "@modules/account/templates/login-template"
-import ErrorMessage from "@modules/checkout/components/error-message"
-import LocalizedClientLink from "@modules/common/components/localized-client-link"
-import { registerWithPhone } from "@lib/data/customer"
-import "react-phone-number-input/style.css"
-import PhoneInput from "react-phone-number-input"
-import { Otp } from "../otp"
-import { useParams } from "next/navigation"
-import { Button } from "@medusajs/ui"
-
-type Props = {
- setCurrentView: (view: LOGIN_VIEW) => void
-}
-
-const RegisterPhone = ({ setCurrentView }: Props) => {
- const [firstName, setFirstName] = useState("")
- const [lastName, setLastName] = useState("")
- const [phone, setPhone] = useState("")
- const [error, setError] = useState("")
- const [loading, setLoading] = useState(false)
- const [enterOtp, setEnterOtp] = useState(false)
- const { countryCode } = useParams() as { countryCode: string }
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault()
- setLoading(true)
- const response = await registerWithPhone({
- firstName,
- lastName,
- phone,
- })
- setLoading(false)
- if (typeof response === "string") {
- setError(response)
- return
- }
-
- setEnterOtp(true)
- }
-
- if (enterOtp) {
- return
- }
-
- // TODO render form
-}
-
-export default RegisterPhone
-```
-
-You create a `RegisterPhone` component that accepts a `setCurrentView` prop to switch between the login and registration views.
-
-In the component, you define the following state variables:
-
-- `firstName`, `lastName`, and `phone` to store the form inputs' values.
-- `error`: The error message to show if the registration fails.
-- `loading`: A boolean indicating whether the registration is in progress.
-- `enterOtp`: A boolean indicating whether to show the OTP form. This is enabled once the customer is registered and they need to authenticate using the OTP.
-- `countryCode`: The country code of the customer, which is retrieved from the URL parameters. You'll use this to show the phone input with a default selected country.
-
-You also define a `handleSubmit` function that handles the form submission. It calls the `registerWithPhone` function to register the customer using their phone number.
-
-If the registration is successful, you set `enterOtp` to `true` to show the OTP form. Otherwise, you set the error message.
-
-#### Render Registration Form
-
-Next, you'll render the registration form with the input fields and the submit button.
-
-Replace the `TODO` with the following:
-
-```tsx title="src/modules/account/components/register-phone/index.tsx" badgeLabel="Storefront" badgeColor="blue"
-return (
-
-
- Become a Medusa Store Member
-
-
- Create your Medusa Store Member profile, and get access to an enhanced
- shopping experience.
-
-
-
- Already a member?{" "}
-
- .
-
-
-)
-```
-
-You render the registration form with input fields for the first name, last name, and phone number.
-
-For the phone number input, you use the `PhoneInput` component from the `react-phone-number-input` package. You set the `defaultCountry` prop to the country code retrieved from the URL parameters.
-
-You also show a submit button that calls the `handleSubmit` function when clicked, and a button to switch to the login form.
-
-#### Add to Login Template
-
-Next, you'll add the `RegisterPhone` component to the login template.
-
-In `src/modules/account/templates/login-template.tsx`, add the following import:
-
-```tsx title="src/modules/account/templates/login-template.tsx" badgeLabel="Storefront" badgeColor="blue"
-import RegisterPhone from "@modules/account/components/register-phone"
-```
-
-Then, change the `return` statement of the `LoginTemplate` component to the following:
-
-```tsx title="src/modules/account/templates/login-template.tsx" badgeLabel="Storefront" badgeColor="blue"
-return (
-
-)
-```
-
-You show the registration form when the `currentView` is set to `register-phone`. You'll also add the login form later.
-
-### h. Add Login Form
-
-Next, you'll add a login form that allows the customer to log in using their phone number.
-
-To create the form, create the file `src/modules/account/components/login-phone/index.tsx` with the following content:
-
-```tsx title="src/modules/account/components/login-phone/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={loginPhoneHighlights}
-"use client"
-
-import { authenticateWithPhone } from "@lib/data/customer"
-import { LOGIN_VIEW } from "@modules/account/templates/login-template"
-import ErrorMessage from "@modules/checkout/components/error-message"
-import { useState } from "react"
-import "react-phone-number-input/style.css"
-import PhoneInput from "react-phone-number-input"
-import { Otp } from "../otp"
-import { useParams } from "next/navigation"
-import { Button } from "@medusajs/ui"
-
-type Props = {
- setCurrentView: (view: LOGIN_VIEW) => void
-}
-
-const LoginPhone = ({ setCurrentView }: Props) => {
- const [phone, setPhone] = useState("")
- const [error, setError] = useState("")
- const [loading, setLoading] = useState(false)
- const [enterOtp, setEnterOtp] = useState(false)
- const { countryCode } = useParams() as { countryCode: string }
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault()
- setLoading(true)
- const response = await authenticateWithPhone(phone)
- setLoading(false)
- if (typeof response === "string") {
- setError(response)
- return
- }
-
- setEnterOtp(true)
- }
-
- if (enterOtp) {
- return
- }
-
- // TODO render form
-}
-
-export default LoginPhone
-```
-
-You create a `LoginPhone` component that accepts a `setCurrentView` prop to switch between the login and registration views.
-
-In the component, you define the following state variables:
-
-- `phone`: The phone number entered by the customer.
-- `error`: The error message to show if the login fails.
-- `loading`: A boolean indicating whether the login is in progress.
-- `enterOtp`: A boolean indicating whether to show the OTP form. This is enabled after the form is submitted.
-- `countryCode`: The country code of the customer, which is retrieved from the URL parameters. You'll use this to show the phone input with a default selected country.
-
-You also define a `handleSubmit` function that handles the form submission. It calls the `authenticateWithPhone` function to authenticate the customer using their phone number. Then, it enables `enterOtp` to show the OTP form.
-
-#### Render Login Form
-
-Next, you'll render the login form with the input field and the submit button.
-
-Replace the `TODO` with the following:
-
-```tsx title="src/modules/account/components/login-phone/index.tsx" badgeLabel="Storefront" badgeColor="blue"
-return (
-
-
Welcome back
-
- Sign in to access an enhanced shopping experience.
-
-
-
- Not a member?{" "}
-
- .
-
-
-)
-```
-
-You render the login form with an input field for the phone number. You also show a submit button that calls the `handleSubmit` function when clicked.
-
-### i. Add to Login Template
-
-Next, you'll add the `LoginPhone` component to the login template.
-
-In `src/modules/account/templates/login-template.tsx`, add the following import:
-
-```tsx title="src/modules/account/templates/login-template.tsx" badgeLabel="Storefront" badgeColor="blue"
-import LoginPhone from "../components/login-phone"
-```
-
-Next, in the `LoginTemplate` component, change the default value of the `currentView` state to `LOGIN_VIEW.SIGN_IN_PHONE`:
-
-```tsx title="src/modules/account/templates/login-template.tsx" badgeLabel="Storefront" badgeColor="blue"
-const [currentView, setCurrentView] = useState(LOGIN_VIEW.SIGN_IN_PHONE)
-```
-
-This ensures the phone login form is shown by default.
-
-Finally, replace the `return` statement of the `LoginTemplate` component to the following:
-
-```tsx title="src/modules/account/templates/login-template.tsx" badgeLabel="Storefront" badgeColor="blue"
-return (
-
-)
-```
-
-You show the login form when the `currentView` is set to `sign-in-phone`.
-
-### Test it Out
-
-You can now test out the phone authentication feature in the Next.js Starter Storefront.
-
-First, start the Medusa application by running the following command in the Medusa project's directory:
-
-```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green"
-npm run dev
-```
-
-Then, start the Next.js Starter Storefront by running the following command in the storefront project's directory:
-
-```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
-npm run dev
-```
-
-Open your browser, navigate to `http://localhost:8000`, and click on the "Account" link at the top right. This will show the login form with just the phone number input.
-
-
-
-You can also switch to the registration form by clicking on the "Join" link below the login form.
-
-
-
-You can try to login with the account you created before, or register with a new one. Once successful, you'll see the OTP form to enter the OTP you received as SMS.
-
-
-
-After you enter the six digits, you'll be logged in and you'll see your profile page.
-
-
-
-***
-
-## Step 5: Disallow Phone Updates
-
-The phone authentication feature is now complete, but there are two improvements you can make:
-
-1. Show the phone number in the profile page: Currently, it shows the email address, which is a fake address you've set.
-2. Disable phone and email updates: Currently, the customer can update their phone number, which is not allowed for phone authentication.
-
-### a. Show Phone Number in Profile Page
-
-To show the phone number in the profile page instead of the email, in `src/modules/account/components/overview/index.tsx`, find the following in the `return` statement:
-
-```tsx title="src/modules/account/components/overview/index.tsx" badgeLabel="Storefront" badgeColor="blue"
-
- {customer?.email}
-
-```
-
-And replace it with the following:
-
-```tsx title="src/modules/account/components/overview/index.tsx" badgeLabel="Storefront" badgeColor="blue"
-
- {customer?.phone}
-
-```
-
-If you check the profile page now, you'll see the phone number instead of the email address at the top right.
-
-
-
-### b. Remove Email and Phone Fields
-
-Next, you'll remove the fields to update email and phone number from the profile page.
-
-In `src/app/[countryCode]/(main)/account/@dashboard/profile/page.tsx`, find the following lines to remove from the `return` statement:
-
-```tsx title="src/app/[countryCode]/(main)/account/@dashboard/profile/page.tsx" badgeLabel="Storefront" badgeColor="blue"
-
- {/* ... */}
- {/* Remove the following */}
-
-
-
-
- {/* ... */}
-
-```
-
-If you go to your profile page and click on "Profile" in the sidebar, the email and phone number sections will be removed.
-
-
-
-### c. Disable Phone Updates in Medusa
-
-While removing the email and phone fields from the profile page prevents customers using the storefront from updating their phone number, it doesn't prevent them from updating it using Medusa's API.
-
-In this section, you'll add a [middleware](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md) to the `/store/customers/me` API route that prevents customers from updating their phone number.
-
-A middleware is a function that's executed whenever a request is sent to an API route. It's executed before the route handler, allowing you to validate requests, apply authentication guards, and more.
-
-Learn more in the [Middlewares](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md) documentation.
-
-To add a middleware in your Medusa application, create the file `src/api/middlewares.ts` with the following content:
-
-```ts title="src/api/middlewares.ts" badgeLabel="Medusa Application" badgeColor="green"
-import { defineMiddlewares } from "@medusajs/framework/http"
-
-export default defineMiddlewares({
- routes: [
- {
- matcher: "/store/customers/me",
- method: ["POST"],
- middlewares: [
- async (req, res, next) => {
- const { phone } = req.body as Record
-
- if (phone) {
- return res.status(400).json({
- error: "Phone number is not allowed to be updated",
- })
- }
-
- next()
- },
- ],
- },
- ],
-})
-```
-
-You define middlewares using the `defineMiddlewares` function. It accepts an object having a `routes` property that holds all middlewares applied to API routes.
-
-Each object in `routes` has the following properties:
-
-- `matcher`: The API route path to apply the middleware on. You set it to `/store/customers/me`.
-- `method`: The HTTP method to apply the middleware to. You set it to `POST` so that the middleware is applied only on `POST` requests sent to the `/store/customers/me` route.
-- `middlewares`: An array of middlewares to apply on the route. You add a middleware that throws an error if the request body contains a `phone` property. This prevents customers from updating their phone number using the API.
-
-Any `POST` request to the `/store/customers/me` route will now be validated to ensure it's not updating the phone number.
-
-***
-
-## Next Steps
-
-You've now implemented phone authentication in Medusa with Twilio integration. You can further customize the phone authentication feature based on your business use case.
-
-### Authenticate Other Actor Types
-
-This tutorial focused on authenticating customers using their phone number. However, you can also authenticate other actor types, such as admin users and vendors.
-
-To do that, first, enable the `phone-auth` authentication strategy in `medusa-config.ts` for the actor types. For example:
-
-```ts title="medusa-config.ts"
-module.exports = defineConfig({
- projectConfig: {
- // ...
- http: {
- // ...
- authMethodsPerActor: {
- user: ["emailpass", "phone-auth"],
- customer: ["emailpass", "phone-auth"],
- vendor: ["emailpass", "phone-auth"],
- },
- },
- },
-})
-```
-
-Then, when sending requests to the authentication API routes mentioned in the [Test Out Phone Authentication](#test-out-phone-authentication) section, replace `customer` in the API route paths with the actor type you want to authenticate:
-
-- `/auth/customer/phone-auth/register` -> `/auth/user/phone-auth/register`
-- `/auth/customer/phone-auth` -> `/auth/user/phone-auth`
-- `/auth/customer/phone-auth/callback` -> `/auth/user/phone-auth/callback`
-
-Finally, update the UI to show the phone authentication option for the actor type you want to authenticate. This depends on the UI you're using, but you can follow an approach similar to the Next.js Starter Storefront customizations.
-
-The login form of Medusa Admin can't be customized, so you'll have to build a custom admin dashboard to support phone authentication for admin users.
-
-### Learn More about Medusa
-
-If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth learning of all the concepts you've used in this guide and more.
-
-To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).
-
-### Troubleshooting
-
-If you encounter issues during your development, check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/index.html.md).
-
-### Getting Help
-
-If you encounter issues not covered in the troubleshooting guides:
-
-1. Visit the [Medusa GitHub repository](https://github.com/medusajs/medusa) to report issues or ask questions.
-2. Join the [Medusa Discord community](https://discord.gg/medusajs) for real-time support from community members.
-3. Contact the [sales team](https://medusajs.com/contact/) to get help from the Medusa team.
-
-
# Send Abandoned Cart Notifications in Medusa
In this tutorial, you will learn how to send notifications to customers who have abandoned their carts.
@@ -50337,6 +48319,510 @@ If you're new to Medusa, check out the [main documentation](https://docs.medusaj
To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).
+# Implement Quick Re-Order Functionality in Medusa
+
+In this tutorial, you'll learn how to implement a re-order functionality in Medusa.
+
+When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md) which are available out-of-the-box. The features include order-management features.
+
+The Medusa Framework facilitates building custom features that are necessary for your business use case. In this tutorial, you'll learn how to implement a re-order functionality in Medusa. This feature is useful for businesses whose customers are likely to repeat their orders, such as B2B or food delivery businesses.
+
+You can follow this guide whether you're new to Medusa or an advanced Medusa developer.
+
+## Summary
+
+By following this tutorial, you'll learn how to:
+
+- Install and set up Medusa.
+- Define the logic to re-order an order.
+- Customize the Next.js Starter Storefront to add a re-order button.
+
+
+
+- [Re-Order Repository](https://github.com/medusajs/examples/tree/main/re-order): Find the full code for this guide in this repository.
+- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1741941475/OpenApi/product-reviews_jh8ohj.yaml): Import this OpenApi Specs file into tools like Postman.
+
+***
+
+## Step 1: Install a Medusa Application
+
+### Prerequisites
+
+- [Node.js v20+](https://nodejs.org/en/download)
+- [Git CLI tool](https://git-scm.com/downloads)
+- [PostgreSQL](https://www.postgresql.org/download/)
+
+Start by installing the Medusa application on your machine with the following command:
+
+```bash
+npx create-medusa-app@latest
+```
+
+You'll first be asked for the project's name. Then, when asked whether you want to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose Yes.
+
+Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js Starter Storefront in a separate directory with the `{project-name}-storefront` name.
+
+The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).
+
+Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterwards, you can log in with the new user and explore the dashboard.
+
+Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.
+
+***
+
+## Step 2: Implement Re-Order Workflow
+
+To build custom commerce features in Medusa, you create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). A workflow is a series of queries and actions, called steps, that complete a task.
+
+By using workflows, you can track their executions' progress, define roll-back logic, and configure other advanced features. Then, you execute the workflow from other customizations, such as in an API Route.
+
+In this section, you'll implement the re-order functionality in a workflow. Later, you'll execute the workflow in a custom API route.
+
+Refer to the [Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) to learn more.
+
+The workflow will have the following steps:
+
+- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the order's details.
+- [createCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartWorkflow/index.html.md): Create a cart for the re-order.
+- [addShippingMethodToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addShippingMethodToCartWorkflow/index.html.md): Add the order's shipping method(s) to the cart.
+- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details.
+
+This workflow uses steps from Medusa's `@medusajs/medusa/core-flows` package. So, you can implement the workflow without implementing custom steps.
+
+### a. Create the Workflow
+
+To create the workflow, create the file `src/workflows/reorder.ts` with the following content:
+
+```ts title="src/workflows/reorder.ts" highlights={workflowHighlights1}
+import {
+ createWorkflow,
+ transform,
+ WorkflowResponse,
+} from "@medusajs/framework/workflows-sdk"
+import {
+ addShippingMethodToCartWorkflow,
+ createCartWorkflow,
+ useQueryGraphStep,
+} from "@medusajs/medusa/core-flows"
+
+type ReorderWorkflowInput = {
+ order_id: string
+}
+
+export const reorderWorkflow = createWorkflow(
+ "reorder",
+ ({ order_id }: ReorderWorkflowInput) => {
+ // @ts-ignore
+ const { data: orders } = useQueryGraphStep({
+ entity: "order",
+ fields: [
+ "*",
+ "items.*",
+ "shipping_address.*",
+ "billing_address.*",
+ "region.*",
+ "sales_channel.*",
+ "shipping_methods.*",
+ "customer.*",
+ ],
+ filters: {
+ id: order_id,
+ },
+ })
+
+ // TODO create a cart with the order's items
+ }
+)
+```
+
+You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter.
+
+It accepts as a second parameter a constructor function, which is the workflow's implementation. The function can accept input, which in this case is an object holding the ID of the order to re-order.
+
+In the workflow's constructor function, so far you use the `useQueryGraphStep` step to retrieve the order's details. This step uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) under the hood, which allows you to query data across [modules](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md).
+
+Refer to the [Query documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to learn more about how to use it.
+
+### b. Create a Cart
+
+Next, you need to create a cart using the old order's details. You can use the `createCartWorkflow` step to create a cart, but you first need to prepare its input data.
+
+Replace the `TODO` in the workflow with the following:
+
+```ts title="src/workflows/reorder.ts" highlights={workflowHighlights2}
+const createInput = transform({
+ orders,
+}, (data) => {
+ return {
+ region_id: data.orders[0].region_id!,
+ sales_channel_id: data.orders[0].sales_channel_id!,
+ customer_id: data.orders[0].customer_id!,
+ email: data.orders[0].email!,
+ billing_address: {
+ first_name: data.orders[0].billing_address?.first_name!,
+ last_name: data.orders[0].billing_address?.last_name!,
+ address_1: data.orders[0].billing_address?.address_1!,
+ city: data.orders[0].billing_address?.city!,
+ country_code: data.orders[0].billing_address?.country_code!,
+ province: data.orders[0].billing_address?.province!,
+ postal_code: data.orders[0].billing_address?.postal_code!,
+ phone: data.orders[0].billing_address?.phone!,
+ },
+ shipping_address: {
+ first_name: data.orders[0].shipping_address?.first_name!,
+ last_name: data.orders[0].shipping_address?.last_name!,
+ address_1: data.orders[0].shipping_address?.address_1!,
+ city: data.orders[0].shipping_address?.city!,
+ country_code: data.orders[0].shipping_address?.country_code!,
+ province: data.orders[0].shipping_address?.province!,
+ postal_code: data.orders[0].shipping_address?.postal_code!,
+ phone: data.orders[0].shipping_address?.phone!,
+ },
+ items: data.orders[0].items?.map((item) => ({
+ variant_id: item?.variant_id!,
+ quantity: item?.quantity!,
+ unit_price: item?.unit_price!,
+ })),
+ }
+})
+
+const { id: cart_id } = createCartWorkflow.runAsStep({
+ input: createInput,
+})
+
+// TODO add the shipping method to the cart
+```
+
+Data manipulation is not allowed in a workflow, as Medusa stores its definition before executing it. Instead, you can use `transform` from the Workflows SDK to manipulate the data.
+
+Learn more about why you can't manipulate data in a workflow and the `transform` function in the [Data Manipulation in Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md).
+
+`transform` accepts the following parameters:
+
+1. The data to use in the transformation function.
+2. A transformation function that accepts the data from the first parameter and returns the transformed data.
+
+In the above code snippet, you use `transform` to create the input for the `createCartWorkflow` step. The input is an object that holds the cart's details, including its items, shipping and billing addresses, and more.
+
+Learn about other input parameters you can pass in the [createCartWorkflow reference](https://docs.medusajs.com/references/medusa-workflows/createCartWorkflow/index.html.md).
+
+After that, you execute the `createCartWorkflow` passing it the transformed input. The workflow returns the cart's details, including its ID.
+
+### c. Add Shipping Methods
+
+Next, you need to add the order's shipping method(s) to the cart. This saves the customer from having to select a shipping method again.
+
+You can use the `addShippingMethodToCartWorkflow` step to add the shipping method(s) to the cart.
+
+Replace the `TODO` in the workflow with the following:
+
+```ts title="src/workflows/reorder.ts" highlights={workflowHighlights3}
+const addShippingMethodToCartInput = transform({
+ cart_id,
+ orders,
+}, (data) => {
+ return {
+ cart_id: data.cart_id,
+ options: data.orders[0].shipping_methods?.map((method) => ({
+ id: method?.shipping_option_id!,
+ data: method?.data!,
+ })) ?? [],
+ }
+})
+
+addShippingMethodToCartWorkflow.runAsStep({
+ input: addShippingMethodToCartInput,
+})
+
+// TODO retrieve and return the cart's details
+```
+
+Again, you use `transform` to prepare the input for the `addShippingMethodToCartWorkflow`. The input includes the cart's ID and the shipping method(s) to add to the cart.
+
+Then, you execute the `addShippingMethodToCartWorkflow` to add the shipping method(s) to the cart.
+
+### d. Retrieve and Return the Cart's Details
+
+Finally, you need to retrieve the cart's details and return them as the workflow's output.
+
+Replace the `TODO` in the workflow with the following:
+
+```ts title="src/workflows/reorder.ts" highlights={workflowHighlights4}
+// @ts-ignore
+const { data: carts } = useQueryGraphStep({
+ entity: "cart",
+ fields: [
+ "*",
+ "items.*",
+ "shipping_methods.*",
+ "shipping_address.*",
+ "billing_address.*",
+ "region.*",
+ "sales_channel.*",
+ "promotions.*",
+ "currency_code",
+ "subtotal",
+ "item_total",
+ "total",
+ "item_subtotal",
+ "shipping_subtotal",
+ "customer.*",
+ "payment_collection.*",
+
+ ],
+ filters: {
+ id: cart_id,
+ },
+}).config({ name: "retrieve-cart" })
+
+return new WorkflowResponse(carts[0])
+```
+
+You execute the `useQueryGraphStep` again to retrieve the cart's details. Since you're re-using a step, you have to rename it using the `config` method.
+
+Finally, you return the cart's details. A workflow must return an instance of `WorkflowResponse`.
+
+The `WorkflowResponse` constructor accepts the workflow's output as a parameter, which is the cart's details in this case.
+
+In the next step, you'll create an API route that exposes the re-order functionality.
+
+***
+
+## Step 3: Create Re-Order API Route
+
+Now that you have the logic to re-order, you need to expose it so that frontend clients, such as a storefront, can use it. You do this by creating an [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md).
+
+An API Route is an endpoint that exposes commerce features to external applications and clients, such as storefronts. You'll create an API route at the path `/store/customers/me/orders/:id` that executes the workflow from the previous step.
+
+Refer to the [API Routes documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) to learn more.
+
+An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`.
+
+So, create the file `src/api/store/customers/me/orders/[id]/route.ts` with the following content:
+
+```ts title="src/api/store/customers/me/orders/[id]/route.ts"
+import {
+ AuthenticatedMedusaRequest,
+ MedusaResponse,
+} from "@medusajs/framework/http"
+import { reorderWorkflow } from "../../../../../../workflows/reorder"
+
+export async function POST(
+ req: AuthenticatedMedusaRequest,
+ res: MedusaResponse
+) {
+ const { id } = req.params
+
+ const { result } = await reorderWorkflow(req.scope).run({
+ input: {
+ order_id: id,
+ },
+ })
+
+ return res.json({
+ cart: result,
+ })
+}
+```
+
+Since you export a `POST` route handler function, you expose a `POST` API route at `/store/customers/me/orders/:id`.
+
+API routes that start with `/store/customers/me` are protected by default, meaning that only authenticated customers can access them. Learn more in the [Protected API Routes documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/protected-routes/index.html.md).
+
+The route handler function accepts two parameters:
+
+1. A request object with details and context on the request, such as path parameters or authenticated customer details.
+2. A response object to manipulate and send the response.
+
+In the route handler function, you execute the `reorderWorkflow`. To execute a workflow, you:
+
+- Invoke it, passing it the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) available in the `req.scope` property.
+ - The Medusa container is a registry of Framework and commerce resources that you can resolve and use in your customizations.
+- Call the `run` method, passing it an object with the workflow's input.
+
+You pass the order ID from the request's path parameters as the workflow's input. Finally, you return the created cart's details in the response.
+
+You'll test out this API route after you customize the Next.js Starter Storefront.
+
+***
+
+## Step 4: Customize the Next.js Starter Storefront
+
+In this step, you'll customize the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) to add a re-order button. You installed the Next.js Starter Storefront in the first step with the Medusa application, but you can also install it separately as explained in the [Next.js Starter Storefront documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md).
+
+The Next.js Starter Storefront provides rich commerce features and a sleek design. You can use it as-is or build on top of it to tailor it for your business's unique use case, design, and customer experience.
+
+The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.
+
+So, if your Medusa application's directory is `medusa-reorder`, you can find the storefront by going back to the parent directory and changing to the `medusa-reorder-storefront` directory:
+
+```bash
+cd ../medusa-reorder-storefront # change based on your project name
+```
+
+To add the re-order button, you will:
+
+- Add a server function that re-orders an order using the API route from the previous step.
+- Add a button to the order details page that calls the server function.
+
+### a. Add the Server Function
+
+You'll add the server function for the re-order functionality in the `src/lib/data/orders.ts` file.
+
+First, add the following import statement to the top of the file:
+
+```ts title="src/lib/data/orders.ts" badgeLabel="Storefront" badgeColor="blue"
+import { setCartId } from "./cookies"
+```
+
+Then, add the function at the end of the file:
+
+```ts title="src/lib/data/orders.ts" badgeLabel="Storefront" badgeColor="blue"
+export const reorder = async (id: string) => {
+ const headers = await getAuthHeaders()
+
+ const { cart } = await sdk.client.fetch(
+ `/store/customers/me/orders/${id}`,
+ {
+ method: "POST",
+ headers,
+ }
+ )
+
+ await setCartId(cart.id)
+
+ return cart
+}
+```
+
+You add a function that accepts the order ID as a parameter.
+
+The function uses the `client.fetch` method of the [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md) to send a request to the API route you created in the previous step.
+
+The JS SDK is already configured in the Next.js Starter Storefront. Refer to the [JS SDK documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md) to learn more about it.
+
+Once the request succeeds, you use the `setCartId` function that's defined in the storefront to set the cart ID in a cookie. This ensures the cart is used across the storefront.
+
+Finally, you return the cart's details.
+
+### b. Add the Re-Order Button Component
+
+Next, you'll add the component that shows the re-order button. You'll later add the component to the order details page.
+
+To create the component, create the file `src/modules/order/components/reorder-action/index.tsx` with the following content:
+
+```tsx title="src/modules/order/components/reorder-action/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={componentHighlights}
+import { Button, toast } from "@medusajs/ui"
+import { reorder } from "../../../../lib/data/orders"
+import { useState } from "react"
+import { useRouter } from "next/navigation"
+
+type ReorderActionProps = {
+ orderId: string
+}
+
+export default function ReorderAction({ orderId }: ReorderActionProps) {
+ const [isLoading, setIsLoading] = useState(false)
+ const router = useRouter()
+
+ const handleReorder = async () => {
+ setIsLoading(true)
+ try {
+ const cart = await reorder(orderId)
+
+ setIsLoading(false)
+ toast.success("Prepared cart to reorder. Proceeding to checkout...")
+ router.push(`/${cart.shipping_address!.country_code}/checkout?step=payment`)
+ } catch (error) {
+ setIsLoading(false)
+ toast.error(`Error reordering: ${error}`)
+ }
+ }
+
+ return (
+
+ )
+}
+```
+
+You create a `ReorderAction` component that accepts the order ID as a prop.
+
+In the component, you render a button that, when clicked, calls a `handleReorder` function. The function calls the `reorder` function you created in the previous step to re-order the order.
+
+If the re-order succeeds, you redirect the user to the payment step of the checkout page. If it fails, you show an error message.
+
+### c. Show Re-Order Button on Order Details Page
+
+Finally, you'll show the `ReorderAction` component on the order details page.
+
+In `src/modules/order/templates/order-details-template.tsx`, add the following import statement to the top of the file:
+
+```tsx title="src/modules/order/templates/order-details-template.tsx" badgeLabel="Storefront" badgeColor="blue"
+import ReorderAction from "../components/reorder-action"
+```
+
+Then, in the return statement of the `OrderDetailsTemplate` component, find the `OrderDetails` component and add the `ReorderAction` component below it:
+
+```tsx title="src/modules/order/templates/order-details-template.tsx" badgeLabel="Storefront" badgeColor="blue"
+
+```
+
+The re-order button will now be shown on the order details page.
+
+### Test it Out
+
+You'll now test out the re-order functionality.
+
+First, to start the Medusa application, run the following command in the Medusa application's directory:
+
+```bash npm2yarn badgeLabel="Medusa application" badgeColor="green"
+npm run dev
+```
+
+Then, in the Next.js Starter Storefront directory, run the following command to start the storefront:
+
+```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
+npm run dev
+```
+
+The storefront will be running at `http://localhost:8000`. Open it in your browser.
+
+To test out the re-order functionality:
+
+- Create an account in the storefront.
+- Add a product to the cart and complete the checkout process to place an order.
+- Go to Account -> Orders, and click on the "See details" button.
+
+
+
+On the order's details page, you'll find a "Reorder" button.
+
+
+
+When you click on the button, a new cart will be created with the order's details, and you'll be redirected to the checkout page where you can complete the purchase.
+
+
+
+***
+
+## Next Steps
+
+You now have a re-order functionality in your Medusa application and Next.js Starter Storefront. You can expand more on this feature based on your use case.
+
+For example, you can add quick orders on the storefront's homepage, allowing customers to quickly re-order their last orders.
+
+If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth learning of all the concepts you've used in this guide and more.
+
+To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).
+
+
# Use Saved Payment Methods During Checkout
In this tutorial, you'll learn how to allow customers to save their payment methods and use them for future purchases.
@@ -51181,6 +49667,2055 @@ 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 Phone Authentication and Integrate Twilio SMS
+
+In this tutorial, you will learn how to implement phone number authentication in your Medusa application.
+
+When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md), which are available out-of-the-box. These features include authentication with custom providers and for custom user or actor types.
+
+In this tutorial, you'll learn how to implement a custom authentication provider that allows customers to log in with their phone number. You'll also integrate [Twilio](https://www.twilio.com/en-us/messaging/channels/sms) to send SMS messages to those customers with the one-time password (OTP) for authentication.
+
+Twilio is just one option to deliver the OTP to the customer. You can integrate a different SMS provider or use a different method to send OTPs.
+
+## Summary
+
+By following this tutorial, you will learn how to:
+
+- Install and set up Medusa.
+- Implement a custom phone authentication provider.
+- Integrate Twilio to send OTPs by SMS.
+- Customize the Next.js Starter Storefront to allow customers to log in with their phone numbers.
+
+You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer.
+
+
+
+While this tutorial focuses on supporting phone authentication for customers, you can use the authentication provider for any actor type, such as admin user or vendor. [At the end of this tutorial](#next-steps), you'll learn how to authenticate other actor types.
+
+- [Phone Authentication Repository](https://github.com/medusajs/examples/tree/main/phone-auth): Find the full code for this guide in this repository.
+- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1747745832/OpenApi/Phone_Auth_g4xsqv.yaml): Import this OpenApi Specs file into tools like Postman.
+
+***
+
+## 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: Implement Phone Authentication Module Provider
+
+In Medusa, you integrate custom authentication providers by creating an [Authentication Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/index.html.md). Then, you can use that provider to authenticate users using custom logic.
+
+In this step, you'll create a Phone Authentication Module Provider that allows users to log in with their phone numbers and an OTP. Later, you'll integrate Twilio to send the OTPs to the users, and customize the storefront to allow customers to log in with their phone numbers.
+
+An Authentication Module Provider doesn't need to handle storing and managing specific user details, such as creating customers or admin users. Instead, it only focuses on the logic of authenticating a type of user using custom logic or integration. You can learn more in the [Auth Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/index.html.md) documentation.
+
+### Prerequisite: Install jsonwebtoken
+
+In the Phone Authentication Module Provider, you'll use the `jsonwebtoken` package to sign and verify the OTPs.
+
+To install the package, run the following command in the Medusa application directory:
+
+```bash npm2yarn
+npm install jsonwebtoken
+npm install @types/jsonwebtoken --save-dev
+```
+
+### a. Create Module Directory
+
+Modules are created under the `src/modules` directory. So, start by creating the directory `src/modules/phone-auth`.
+
+### b. Create Auth Module Provider Service
+
+A module has a service that contains its logic. For Authentication Module Providers, the service implements the logic to authenticate users.
+
+To create the service of the Phone Authentication Module Provider, create the file `src/modules/phone-auth/service.ts` with the following content:
+
+```ts title="src/modules/phone-auth/service.ts" highlights={phoneAuthServiceHighlights}
+import {
+ AbstractAuthModuleProvider,
+ AbstractEventBusModuleService,
+} from "@medusajs/framework/utils"
+import {
+ Logger,
+} from "@medusajs/types"
+
+type InjectedDependencies = {
+ logger: Logger
+ event_bus: AbstractEventBusModuleService
+}
+
+type Options = {
+ jwtSecret: string
+}
+
+class PhoneAuthService extends AbstractAuthModuleProvider {
+ static DISPLAY_NAME = "Phone Auth"
+ static identifier = "phone-auth"
+ private options: Options
+ private logger: Logger
+ private event_bus: AbstractEventBusModuleService
+
+ constructor(container: InjectedDependencies, options: Options) {
+ // @ts-ignore
+ super(...arguments)
+
+ this.options = options
+ this.logger = container.logger
+ this.event_bus = container.event_bus
+ }
+}
+
+export default PhoneAuthService
+```
+
+An Authentication Module Provider's service must extend the `AbstractAuthModuleProvider` class. You'll get a type error about implementing the abstract methods of that class, which you'll add in the next steps.
+
+An Authentication Module Provider must also have the following static properties:
+
+- `identifier`: A unique identifier for the provider.
+- `DISPLAY_NAME`: A human-readable name for the provider. This name is used for display purposes.
+
+A module provider's constructor receives two parameters:
+
+- `container`: The [module's container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md) that contains Framework resources available to the module. You access the following resources:
+ - `logger`: A [Logger](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md) class to log debug messages.
+ - `event_bus`: The [Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/index.html.md)'s service to emit events.
+- `options`: Options that are passed to the module provider when it's registered in Medusa's configurations. You define the following option:
+ - `jwtSecret`: A secret used to sign and verify the OTPs.
+
+You'll learn how to set this option when you [add the module provider to Medusa's configurations](#h-add-module-provider-to-medusas-configurations).
+
+In the constructor, you set the class's properties to the injected dependencies and options.
+
+In the next sections, you'll implement the methods of the `AbstractAuthModuleProvider` class.
+
+Refer to the [Create Auth Module Provider](https://docs.medusajs.com/references/auth/provider/index.html.md) guide for detailed information about the methods.
+
+### c. Implement validateOptions Method
+
+The `validateOptions` method is used to validate the options passed to the module provider. If the method throws an error, the Medusa application won't start.
+
+So, add the `validateOptions` method to the `PhoneAuthService` class:
+
+```ts title="src/modules/phone-auth/service.ts"
+// other imports...
+import {
+ MedusaError,
+} from "@medusajs/framework/utils"
+
+class PhoneAuthService extends AbstractAuthModuleProvider {
+ // ...
+ static validateOptions(options: Record): void | never {
+ if (!options.jwtSecret) {
+ throw new MedusaError(
+ MedusaError.Types.INVALID_DATA,
+ "JWT secret is required"
+ )
+ }
+ }
+}
+```
+
+The `validateOptions` method receives the options passed to the module provider as a parameter.
+
+In the method, you throw an error if the `jwtSecret` option is not set.
+
+### d. Implement register Method
+
+When a customer (or another actor type) registers in your application, they must also have an [auth identity](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-identity-and-actor-types/index.html.md) that allows them to login.
+
+The `register` method of an auth provider uses custom logic to create the auth identity for the actor type (such as customer). In the method, you can perform custom validation and specify the custom authentication details to store for the user's auth identity.
+
+Medusa uses the `register` method to create an auth identity that will be associated with the customer when they register. You can learn more in the [Authentication Flows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-flows/index.html.md) documentation.
+
+
+
+So, add the `register` method to the `PhoneAuthService` class:
+
+```ts title="src/modules/phone-auth/service.ts" highlights={registerHighlights}
+// other imports...
+import {
+ AuthenticationInput,
+ AuthIdentityProviderService,
+ AuthenticationResponse,
+} from "@medusajs/types"
+
+class PhoneAuthService extends AbstractAuthModuleProvider {
+ // ...
+ async register(
+ data: AuthenticationInput,
+ authIdentityProviderService: AuthIdentityProviderService
+ ): Promise {
+ const { phone } = data.body || {}
+
+ if (!phone) {
+ return {
+ success: false,
+ error: "Phone number is required",
+ }
+ }
+
+ try {
+ await authIdentityProviderService.retrieve({
+ entity_id: phone,
+ })
+
+ return {
+ success: false,
+ error: "User with phone number already exists",
+ }
+ } catch (error) {
+ const user = await authIdentityProviderService.create({
+ entity_id: phone,
+ })
+
+ return {
+ success: true,
+ authIdentity: user,
+ }
+ }
+ }
+}
+```
+
+#### Parameters
+
+The `register` method receives an object parameter with the following properties:
+
+- `data`: An object containing properties like `body` that holds request-body parameters. Clients will pass relevant authentication data, such as the user's phone number, in the request body.
+- `authIdentityProviderService`: A service injected by the [Auth Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/index.html.md) that allows you to manage auth identities.
+
+The method receives other parameters, which you can find in the [Create Auth Module Provider](https://docs.medusajs.com/references/auth/provider#register/index.html.md) guide.
+
+#### Method Logic
+
+In the method, you extract the `phone` property from the request body, and return an error if it's not provided. You also return an error if another user is using the same phone number.
+
+Otherwise, you create a new auth identity for the user. You set the phone number as the `entity_id` of the auth identity, which is a unique identifier.
+
+#### Return Value
+
+Finally, you return an object with the following properties:
+
+- `success`: A boolean indicating whether the registration was successful.
+- `authIdentity`: The created auth identity of the user. This property is only set if the registration was successful.
+- `error`: An error message if the registration failed.
+
+### e. Implement authenticate Method
+
+When a customer (or another actor type) logs in, the `authenticate` method of an auth provider is called. This method uses custom logic to authenticate the user.
+
+Authentication providers may implement one of the following flows:
+
+- Direct authentication, where the user is authenticated using this method only. For example, authenticating with an email and password.
+- Authentication with callback verification, where the user is authenticated using this method and then a callback is used to verify additional information.
+
+For the Phone Authentication Module Provider, you'll implement the second flow. The user will first be authenticated using the `authenticate` method to make sure the user exists and generate an OTP. Then, they need to supply the OTP to verify their identity.
+
+
+
+So, add the `authenticate` method to the `PhoneAuthService` class:
+
+```ts title="src/modules/phone-auth/service.ts" highlights={authenticateHighlights}
+// other imports...
+import {
+ AuthIdentityDTO,
+} from "@medusajs/types"
+import jwt from "jsonwebtoken"
+
+class PhoneAuthService extends AbstractAuthModuleProvider {
+ // ...
+ async authenticate(
+ data: AuthenticationInput,
+ authIdentityProviderService: AuthIdentityProviderService
+ ): Promise {
+ const { phone } = data.body || {}
+
+ if (!phone) {
+ return {
+ success: false,
+ error: "Phone number is required",
+ }
+ }
+
+ try {
+ await authIdentityProviderService.retrieve({
+ entity_id: phone,
+ })
+ } catch (error) {
+ return {
+ success: false,
+ error: "User with phone number does not exist",
+ }
+ }
+
+ const { hashedOTP, otp } = await this.generateOTP()
+
+ await authIdentityProviderService.update(phone, {
+ provider_metadata: {
+ otp: hashedOTP,
+ },
+ })
+
+ await this.event_bus.emit({
+ name: "phone-auth.otp.generated",
+ data: {
+ otp,
+ phone,
+ },
+ }, {})
+
+ return {
+ success: true,
+ location: "otp",
+ }
+ }
+
+ async generateOTP(): Promise<{ hashedOTP: string, otp: string }> {
+ // Generate a 6-digit OTP
+ const otp = Math.floor(100000 + Math.random() * 900000).toString()
+
+ // for debug
+ this.logger.info(`Generated OTP: ${otp}`)
+
+ const hashedOTP = jwt.sign({ otp }, this.options.jwtSecret, {
+ expiresIn: "60s",
+ })
+
+ return { hashedOTP, otp }
+ }
+}
+```
+
+You add two methods: the `authenticate` method, and a helper `generateOTP` method.
+
+#### authenticate Parameters
+
+The `authenticate` method receives an object parameter with the following properties:
+
+- `data`: An object containing properties like `body` that holds request-body parameters. Clients will pass relevant authentication data, such as the user's phone number, in the request body.
+- `authIdentityProviderService`: A service injected by the [Auth Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/index.html.md) that allows you to manage auth identities.
+
+The method receives other parameters, which you can find in the [Create Auth Module Provider](https://docs.medusajs.com/references/auth/provider#authenticate/index.html.md) guide.
+
+#### authenticate Logic
+
+In the method, you return an error if the `phone` property is not provided in the request body, or if a user with that phone number doesn't exist.
+
+Next, you generate a 6-digit OTP using the `generateOTP` method. Notice that you currently log the OTP for debugging purposes. You can remove this line later once you integrate Twilio.
+
+The OTP is hashed and stored in the `provider_metadata` property of the user's auth identity. The `provider_metadata` property is a JSON object that stores additional information about the auth identity.
+
+Then, you emit an event with the generated OTP and the user's phone number. This allows you later to handle the event and send the OTP to the user using services like Twilio.
+
+#### authenticate Return Value
+
+Finally, you return an object with the following properties:
+
+- `success`: A boolean indicating whether the authentication was successful.
+- `location`: A string indicating a URL to perform additional actions in. In this case, you set the location to `otp`, indicating that the user should verify with the OTP.
+- `error`: An error message if the authentication failed.
+
+### f. Implement validateCallback Method
+
+When an authentication provider requires a callback to verify the user, the Medusa application calls the `validateCallback` method.
+
+You can use this method to verify the OTP that the user entered. If valid, you return the logged in user, and the Medusa application will return a JWT token that the user can use to authenticate in the application.
+
+
+
+So, add the `validateCallback` method to the `PhoneAuthService` class:
+
+```ts title="src/modules/phone-auth/service.ts" highlights={validateCallbackHighlights}
+class PhoneAuthService extends AbstractAuthModuleProvider {
+ // ...
+ async validateCallback(
+ data: AuthenticationInput,
+ authIdentityProviderService: AuthIdentityProviderService
+ ): Promise {
+ const { phone, otp } = data.query || {}
+
+ if (!phone || !otp) {
+ return {
+ success: false,
+ error: "Phone number and OTP are required",
+ }
+ }
+
+ const user = await authIdentityProviderService.retrieve({
+ entity_id: phone,
+ })
+
+ if (!user) {
+ return {
+ success: false,
+ error: "User with phone number does not exist",
+ }
+ }
+
+ // verify that OTP is correct
+ const userProvider = user.provider_identities?.find((provider) => provider.provider === this.identifier)
+ if (!userProvider || !userProvider.provider_metadata?.otp) {
+ return {
+ success: false,
+ error: "User with phone number does not have a phone auth provider",
+ }
+ }
+
+ try {
+ const decodedOTP = jwt.verify(
+ userProvider.provider_metadata.otp as string,
+ this.options.jwtSecret
+ ) as { otp: string }
+
+ if (decodedOTP.otp !== otp) {
+ throw new Error("Invalid OTP")
+ }
+ } catch (error) {
+ return {
+ success: false,
+ error: error.message || "Invalid OTP",
+ }
+ }
+
+ const updatedUser = await authIdentityProviderService.update(phone, {
+ provider_metadata: {
+ otp: null,
+ },
+ })
+
+ return {
+ success: true,
+ authIdentity: updatedUser,
+ }
+ }
+}
+```
+
+#### Parameters
+
+The `validateCallback` method receives an object parameter with the following properties:
+
+- `data`: An object containing properties like `query` that holds query parameters. Clients will pass relevant authentication data, such as the user's phone number and OTP, in the request query.
+- `authIdentityProviderService`: A service injected by the [Auth Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/index.html.md) that allows you to manage auth identities.
+
+The method receives other parameters, which you can find in the [Create Auth Module Provider](https://docs.medusajs.com/references/auth/provider#validatecallback/index.html.md) guide.
+
+#### Method Logic
+
+In the method, you return an error if the phone and otp aren't provided in the request query, or if a user with that phone number doesn't exist.
+
+Next, you verify that the OTP provided by the user is correct. You retrieve the hashed OTP from the `provider_metadata` property of the user's auth identity. If the OTP is not valid, you return an error.
+
+Since you set the hash expiration to 60 seconds, the OTP will be valid for 60 seconds. After that, the user will need to request a new OTP.
+
+After that, you update the user's auth identity to remove the OTP from the `provider_metadata` property.
+
+#### Return Value
+
+Finally, you return an object with the following properties:
+
+- `success`: A boolean indicating whether the authentication was successful.
+- `authIdentity`: The user's auth identity. This property is only set if the authentication was successful.
+- `error`: An error message if the authentication failed.
+
+### g. Export Module Definition
+
+You've now finished implementing the necessary methods for the Phone Authentication Module Provider.
+
+The final piece to a module is its definition, which you export in an `index.ts` file at the module's root directory. This definition tells Medusa the name of the module, its service, and optionally its loaders.
+
+To create the module's definition, create the file `src/modules/phone-auth/index.ts` with the following content:
+
+```ts title="src/modules/phone-auth/index.ts"
+import PhoneAuthService from "./service"
+import {
+ ModuleProvider,
+ Modules,
+} from "@medusajs/framework/utils"
+
+export default ModuleProvider(Modules.AUTH, {
+ services: [PhoneAuthService],
+})
+```
+
+You use `ModuleProvider` from the Modules SDK to create the module provider's definition. It accepts two parameters:
+
+1. The name of the module that this provider belongs to, which is `Modules.AUTH` in this case.
+2. An object with a required property `services` indicating the module provider's services. Each of these services will be registered as authentication providers in Medusa.
+
+### h. Add Module Provider to Medusa's Configurations
+
+Once you finish building the module, add it to Medusa's configurations to start using it.
+
+In `medusa-config.ts`, add a `modules` property:
+
+```ts title="medusa-config.ts"
+// other imports...
+import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils"
+
+module.exports = defineConfig({
+ // ...
+ modules: [
+ {
+ resolve: "@medusajs/medusa/auth",
+ dependencies: [
+ Modules.CACHE,
+ ContainerRegistrationKeys.LOGGER,
+ Modules.EVENT_BUS,
+ ],
+ options: {
+ providers: [
+ // default provider
+ {
+ resolve: "@medusajs/medusa/auth-emailpass",
+ id: "emailpass",
+ },
+ {
+ resolve: "./src/modules/phone-auth",
+ id: "phone-auth",
+ options: {
+ jwtSecret: process.env.PHONE_AUTH_JWT_SECRET || "supersecret",
+ },
+ },
+ ],
+ },
+ },
+ ],
+})
+```
+
+To pass an Auth Module Provider to the Auth Module, you add the `modules` property to the Medusa configuration and pass the Auth Module in its value.
+
+The Auth Module accepts a `dependencies` option, allowing you to inject dependencies into the containers of the module and its providers. The Auth Module requires passing the [Cache Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/index.html.md) and Logger, but you also inject the `event_bus` dependency to use the Event Module's service in the Phone Authentication Module Provider.
+
+The Auth Module also accepts a `providers` option, which is an array of Auth Module Providers to register. You register the `emailpass` provider, which is registered by default when you don't provide any other providers.
+
+To register the Phone Authentication Module Provider, you add an object to the `providers` array with the following properties:
+
+- `resolve`: The NPM package or path to the module provider. In this case, it's the path to the `src/modules/phone-auth` directory.
+- `id`: The ID of the module provider. The auth provider is then registered with the ID `au_{id}`.
+- `options`: The options to pass to the module provider. These are the options you defined in the `Options` interface of the module provider's service.
+
+### i. Enable Phone Authentication for Customers
+
+By default, customers and admin users can be authenticated using the `emailpass` provider. When you add a new provider, you need to specify which actor types can use it.
+
+In `medusa-config.ts`, add to `projectConfig.http` a new `authMethodsPerActor` property:
+
+```ts title="medusa-config.ts" highlights={authMethodsPerActorHighlights}
+module.exports = defineConfig({
+ projectConfig: {
+ // ...
+ http: {
+ // ...
+ authMethodsPerActor: {
+ user: ["emailpass"],
+ customer: ["emailpass", "phone-auth"],
+ },
+ },
+ },
+ // ...
+})
+```
+
+The `authMethodsPerActor` property is an object whose keys are actor types. The values are arrays of authentication method IDs that can be used for that actor type.
+
+In this case, you enable the `phone-auth` provider for customers. You can also enable it for other actor types, such as admin users or vendors.
+
+### Test Out Phone Authentication
+
+In this section, you'll test out the Phone Authentication Module Provider using Medusa's API routes. You can, instead, test it out later using the [Next.js Starter Storefront](#step-4-use-phone-authentication-in-the-nextjs-starter-storefront).
+
+First, start the Medusa application with the following command:
+
+```bash npm2yarn
+npm run start
+```
+
+#### Prerequisite: Retrieve Publishable API Key
+
+Before you start testing the authentication provider using the API routes, you need to retrieve your application's publishable API key. This key is necessary to send requests to API routes starting with `/store`.
+
+To retrieve the publishable API key:
+
+1. Open the Medusa Admin dashboard at `http://localhost:9000/admin` and log in.
+2. Go to Settings -> Publishable API Keys.
+3. Click on the API key in the table.
+4. In its details page, click on the API key to copy it.
+
+
+
+#### a. Retrieve Registration Token
+
+The first step is to retrieve a registration token for a new customer. This token will allow them to register in the application.
+
+To retrieve the registration token, send a `POST` request to `/auth/customer/phone-auth/register`:
+
+```bash
+curl -X POST 'http://localhost:9000/auth/customer/phone-auth/register' \
+--header 'Content-Type: application/json' \
+--data '{
+ "phone": "+19077890116"
+}'
+```
+
+Make sure to replace the phone number with the one you want to use.
+
+This will return a `token` in the response:
+
+```json title="Example Response"
+{
+ "token": "123..."
+}
+```
+
+#### b. Register Customer
+
+Next, you'll register the customer using the [Register Customer](https://docs.medusajs.com/api/store#customers_postcustomers) API route. You'll pass the registration token you received in the previous step in the header of this request.
+
+So, send a `POST` request to `/store/customers`:
+
+```bash
+curl -X POST 'http://localhost:9000/store/customers' \
+--header 'x-publishable-api-key: {publishable_api_key}' \
+--header 'Content-Type: application/json' \
+--header 'Authorization: Bearer {reg_token}' \
+--data-raw '{
+ "email": "+19077890116@gmail.com",
+ "phone": "19077890116",
+ "first_name": "John",
+ "last_name": "Smith"
+}'
+```
+
+Make sure to replace:
+
+- `{publishable_api_key}` with the publishable API key you retrieved from the Medusa Admin dashboard.
+- `{reg_token}` with the registration token you received in the previous step.
+- The customer details in the request body with the ones you want to use. Use the same phone number you used in the previous step.
+ - You pass the email because it's required by the [Register Customer](https://docs.medusajs.com/api/store#customers_postcustomers) API route. You set it to the phone number with a `gmail.com` domain.
+
+The request will return the created customer's details:
+
+```json title="Example Response"
+{
+ "customer": {
+ "id": "cus_01JVPESW5SM1MSVPNM2MSC0ZEC",
+ "email": "+19077890116@gmail.com",
+ "company_name": null,
+ "first_name": "John",
+ "last_name": "Smith",
+ "phone": "19077890116",
+ "metadata": null,
+ "has_account": true,
+ "deleted_at": null,
+ "created_at": "2025-05-20T09:01:13.273Z",
+ "updated_at": "2025-05-20T09:01:13.273Z",
+ "addresses": []
+ }
+}
+```
+
+The customer can now authenticate using the phone number and OTP.
+
+#### c. Authenticate Customer
+
+Next, you'll authenticate the customer using the [Authenticate Customer](https://docs.medusajs.com/api/store#auth_postactor_typeauth_provider) API route. This would send the customer an OTP to their phone number (which you'll implement in the next step).
+
+So, send a `POST` request to `/auth/customer/phone-auth`:
+
+```bash
+curl -X POST 'http://localhost:9000/auth/customer/phone-auth' \
+--header 'Content-Type: application/json' \
+--data '{
+ "phone": "+19077890116"
+}'
+```
+
+Make sure to replace the phone number with the one you used to register the customer.
+
+This will return a `location` in the response:
+
+```json title="Example Response"
+{
+ "location": "otp"
+}
+```
+
+Indicating that the user should verify their OTP.
+
+You can also use this route to resend the OTP if the user didn't receive it or if a minute has passed since the last OTP was sent.
+
+#### d. Verify OTP
+
+If you check the logs of the Medusa application, you'll see that the OTP was generated and logged:
+
+```bash
+info: Generated OTP: 576794
+```
+
+As mentioned before, this is only for debugging purposes. In the next step, you'll implement the logic to send the OTP to the user using Twilio.
+
+So, to verify the OTP, you'll send a request to the [Verify Callback API route](https://docs.medusajs.com/api/store#auth_postactor_typeauth_providercallback):
+
+```bash
+curl -X POST 'http://localhost:9000/auth/customer/phone-auth/callback?phone=%2B19077890116&otp=476588'
+```
+
+You pass the following query parameters:
+
+- `phone`: The phone number of the customer. Make sure to use the same phone number you used to register the customer, and to encode it. For example, the `+` sign should be encoded as `%2B`.
+- `otp`: The OTP that the customer received. Make sure to use the same OTP shown in the logs.
+
+If the OTP is valid, you'll receive a JWT token in the response:
+
+```json title="Example Response"
+{
+ "token": "123..."
+}
+```
+
+You can use this token to authenticate the customer in the application. For example, you can use the token to [retrieve the customer's details](https://docs.medusajs.com/api/store#customers_getcustomersme).
+
+If the OTP has expired, send a request to the [Authenticate Customer](#c-authenticate-customer) API route to generate a new OTP
+
+***
+
+## Step 3: Integrate Twilio SMS
+
+Similar to the Auth Module, the [Notification Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/index.html.md) allows registering custom providers to send notifications, such as SMS or email.
+
+In this step, you'll create a Twilio Notification Module Provider, then use it to send the OTP to the customer.
+
+### Prerequisites
+
+- [Twilio Account](https://console.twilio.com/)
+- [Twilio From Phone Number](https://www.twilio.com/docs/phone-numbers)
+- [Twilio Account SID, which you can retrieve from the Twilio Console homepage.](https://www.twilio.com/docs/usage/tutorials/how-to-use-your-free-trial-account-namer#console-dashboard-home-page)
+- [Twilio Auth Token, which you can retrieve from the Twilio Console homepage.](https://www.twilio.com/docs/usage/tutorials/how-to-use-your-free-trial-account-namer#console-dashboard-home-page)
+
+### a. Install Twilio SDK
+
+Before you start implementing the Twilio Notification Module Provider, install the Twilio SDK to interact with the Twilio API.
+
+Run the following command in the Medusa application directory:
+
+```bash npm2yarn
+npm install twilio
+```
+
+You'll use the Twilio SDK in the Notification Module Provider's service.
+
+### b. Create Module Directory
+
+Create the directory `src/modules/twilio-sms` to create the Twilio Notification Module Provider.
+
+### c. Create Notification Module Provider Service
+
+A Notification Module Provider has a service that contains the sending logic. The service must extend the `AbstractNotificationProviderService` class.
+
+So, create the file `src/modules/twilio-sms/service.ts` with the following content:
+
+```ts title="src/modules/twilio-sms/service.ts" highlights={twilioSmsServiceHighlights}
+import {
+ AbstractNotificationProviderService,
+} from "@medusajs/framework/utils"
+import { Twilio } from "twilio"
+
+type InjectedDependencies = {}
+
+type TwilioSmsServiceOptions = {
+ accountSid: string
+ authToken: string
+ from: string
+}
+
+class TwilioSmsService extends AbstractNotificationProviderService {
+ static readonly identifier = "twilio-sms"
+ private readonly client: Twilio
+ private readonly from: string
+
+ constructor(container: InjectedDependencies, options: TwilioSmsServiceOptions) {
+ super()
+
+ this.client = new Twilio(options.accountSid, options.authToken)
+ this.from = options.from
+ }
+}
+```
+
+You'll get a type error about implementing the abstract methods of the `AbstractNotificationProviderService` class, which you'll add in the next steps.
+
+A Notification Module Provider must have an `identifier` static property, which is a unique identifier for the module. This identifier is used to register the module in the Medusa application.
+
+A module provider's constructor receives two parameters:
+
+- `container`: The [module's container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md) that contains Framework resources available to the module. You don't need to access any resources for this provider.
+- `options`: Options that are passed to the module provider when it's registered in Medusa's configurations. You define the following option:
+ - `accountSid`: The Twilio account SID.
+ - `authToken`: The Twilio auth token.
+ - `from`: The Twilio phone number to send the SMS from.
+
+You'll learn how to set these options when you [add the module provider to Medusa's configurations](#g-add-module-provider-to-medusas-configurations).
+
+In the constructor, you set the class's properties to the injected dependencies and options.
+
+In the next sections, you'll implement the methods of the `AbstractNotificationProviderService` class.
+
+Refer to the [Create Notification Module Provider](https://docs.medusajs.com/references/notification-provider-module/index.html.md) guide for detailed information about the methods.
+
+### d. Implement validateOptions Method
+
+The `validateOptions` method is used to validate the options passed to the module provider. If the method throws an error, the Medusa application won't start.
+
+So, add the `validateOptions` method to the `TwilioSmsService` class:
+
+```ts title="src/modules/twilio-sms/service.ts"
+class TwilioSmsService extends AbstractNotificationProviderService {
+ // ...
+ static validateOptions(options: Record): void | never {
+ if (!options.accountSid) {
+ throw new Error("Account SID is required")
+ }
+ if (!options.authToken) {
+ throw new Error("Auth token is required")
+ }
+ if (!options.from) {
+ throw new Error("From is required")
+ }
+ }
+}
+```
+
+The `validateOptions` method receives the options passed to the module provider as a parameter.
+
+In the method, you throw an error if any of the options are not set.
+
+### e. Implement send Method
+
+The only required method for a Notification Module Provider is the `send` method. When the Medusa application needs to send a notification using the provider's channel (such as SMS), it calls this method of the registered provider.
+
+So, add the `send` method to the `TwilioSmsService` class:
+
+```ts title="src/modules/twilio-sms/service.ts" highlights={sendHighlights}
+// other imports...
+import {
+ ProviderSendNotificationDTO,
+ ProviderSendNotificationResultsDTO,
+} from "@medusajs/types"
+
+class TwilioSmsService extends AbstractNotificationProviderService {
+ // ...
+ async send(
+ notification: ProviderSendNotificationDTO
+ ): Promise {
+ const { to, content, template, data } = notification
+ const contentText = content?.text || await this.getTemplateContent(
+ template, data
+ )
+
+ const message = await this.client.messages.create({
+ body: contentText,
+ from: this.from,
+ to,
+ })
+
+ return {
+ id: message.sid,
+ }
+ }
+
+ async getTemplateContent(
+ template: string,
+ data?: Record | null
+ ): Promise {
+ switch (template) {
+ case "otp-template":
+ if (!data?.otp) {
+ throw new Error("OTP is required for OTP template")
+ }
+
+ return `Your OTP is ${data.otp}`
+ default:
+ throw new Error(`Template ${template} not found`)
+ }
+ }
+}
+```
+
+You implement the `send` method and a helper `getTemplateContent` method.
+
+#### send Parameters
+
+The `send` method receives an object parameter with the following properties:
+
+- `to`: The phone number to send the SMS to.
+- `content`: An object containing the content of the SMS. The `text` property is the text to send.
+- `template`: The template to use for the SMS. This is used to retrieve the fallback content of the SMS if `content.text` is not provided.
+- `data`: An object containing the data to use in the template. This is used to replace placeholders in the template with actual values.
+
+The method receives other parameters, which you can find in the [Create Notification Module Provider](https://docs.medusajs.com/references/notification-provider-module#send/index.html.md) guide.
+
+#### send Method Logic
+
+In the method, you set the SMS content either to the `text` property of the `content` object or to the template content. You define a `getTemplateContent` method that retrieves the content for a template.
+
+Then, you use the `messages.create` method of the Twilio client to send the SMS. You pass the following parameters:
+
+- `body`: The content of the SMS.
+- `from`: The Twilio phone number to send the SMS from.
+- `to`: The phone number to send the SMS to.
+
+#### send Return Value
+
+Finally, you return an object that has an `id` property with the ID of the sent SMS. This ID is stored in the notification record in the database.
+
+### f. Export Module Definition
+
+You've now finished implementing the necessary methods for the Twilio Notification Module Provider. You only need to export its definition.
+
+To create the module's definition, create the file `src/modules/twilio-sms/index.ts` with the following content:
+
+```ts title="src/modules/twilio-sms/index.ts"
+import {
+ ModuleProvider,
+ Modules,
+} from "@medusajs/framework/utils"
+import TwilioSMSNotificationService from "./service"
+
+export default ModuleProvider(Modules.NOTIFICATION, {
+ services: [TwilioSMSNotificationService],
+})
+```
+
+You use `ModuleProvider` from the Modules SDK to create the module provider's definition passing it two parameters:
+
+1. The name of the module that this provider belongs to, which is `Modules.NOTIFICATION` in this case.
+2. An object with a required property `services` indicating the module provider's services. Each of these services will be registered as notification providers in Medusa.
+
+### g. Add Module Provider to Medusa's Configurations
+
+You'll now add the Twilio Notification Module Provider to Medusa's configurations to start using it.
+
+In `medusa-config.ts`, add the following to the `modules` property:
+
+```ts title="medusa-config.ts"
+module.exports = defineConfig({
+ // ...
+ modules: [
+ // ...
+ {
+ resolve: "@medusajs/medusa/notification",
+ options: {
+ providers: [
+ // default provider
+ {
+ resolve: "@medusajs/medusa/notification-local",
+ id: "local",
+ options: {
+ name: "Local Notification Provider",
+ channels: ["feed"],
+ },
+ },
+ {
+ resolve: "./src/modules/twilio-sms",
+ id: "twilio-sms",
+ options: {
+ channels: ["sms"],
+ accountSid: process.env.TWILIO_ACCOUNT_SID,
+ authToken: process.env.TWILIO_AUTH_TOKEN,
+ from: process.env.TWILIO_FROM,
+ },
+ },
+ ],
+ },
+ },
+ ],
+})
+```
+
+You pass the Notification Module in the `modules` property to register the Twilio Notification Module Provider.
+
+The Notification Module accepts a `providers` option, which is an array of Notification Module Providers to register. You register the `local` provider, which is registered by default when you don't provide any other providers.
+
+To register the Twilio Notification Module Provider, you add an object with the following properties:
+
+- `resolve`: The path to the module provider.
+- `id`: The ID of the module provider. The notification provider is then registered with the ID `np_{identifier}_{id}`.
+- `options`: The options to pass to the module provider. These include the options you defined in the `Options` interface of the module provider's service.
+ - `channels`: The channels that the notification provider supports. In this case, you set it to `sms`, which is the channel used to send SMS notifications.
+ - `accountSid`: The Twilio account SID.
+ - `authToken`: The Twilio auth token.
+ - `from`: The Twilio phone number to send the SMS from.
+
+### h. Add Environment Variables
+
+To set the value of the Twilio options, add the following environment variables to your `.env` file:
+
+```shell title=".env"
+TWILIO_ACCOUNT_SID=AC...
+TWILIO_AUTH_TOKEN=05...
+TWILIO_FROM=+1...
+```
+
+Where:
+
+- `TWILIO_ACCOUNT_SID`: The Twilio account SID.
+- `TWILIO_AUTH_TOKEN`: The Twilio auth token.
+- `TWILIO_FROM`: The Twilio phone number to send the SMS from. Make sure to use the phone number you purchased from Twilio.
+
+You can retrieve these information from the Twilio Console homepage.
+
+
+
+### i. Handle OTP Generated Event
+
+Now that you have integrated Twilio into Medusa, you can use it to send the OTP to the customer. To do that, you need to handle the `phone-auth.otp.generated` event that you emitted in the `authenticate` method of the Phone Authentication Module Provider.
+
+You can listen to events in a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md). A subscriber is an asynchronous function that listens to events to perform actions when the event is emitted.
+
+In this step, you'll create a subscriber that listens to the `phone-auth.otp.generated` event and sends an SMS to the customer with the OTP.
+
+Refer to the [Events and Subscribers](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) documentation to learn more.
+
+Subscribers are created in a TypeScript or JavaScript file under the `src/subscribers` directory. So, to create a subscriber, create the file `src/subscribers/send-otp.ts` with the following content:
+
+```ts title="src/subscribers/send-otp.ts" highlights={sendOtpHighlights}
+import {
+ SubscriberArgs,
+ type SubscriberConfig,
+} from "@medusajs/medusa"
+import { Modules } from "@medusajs/framework/utils"
+
+export default async function sendOtpHandler({
+ event: { data: {
+ phone,
+ otp,
+ } },
+ container,
+}: SubscriberArgs<{ phone: string, otp: string }>) {
+ const notificationModuleService = container.resolve(
+ Modules.NOTIFICATION
+ )
+
+ await notificationModuleService.createNotifications({
+ to: phone,
+ channel: "sms",
+ template: "otp-template",
+ data: {
+ otp,
+ },
+ })
+}
+
+export const config: SubscriberConfig = {
+ event: "phone-auth.otp.generated",
+}
+```
+
+The subscriber file must export:
+
+- An asynchronous subscriber function that's executed whenever the associated event is triggered.
+- A configuration object with an event property whose value is the event the subscriber is listening to, which is `phone-auth.otp.generated`.
+
+The subscriber function accepts an object with the following properties:
+
+- `event`: An object with the event's data payload. In the `authenticate` method, you emitted the event with the following data:
+ - `phone`: The phone number of the user.
+ - `otp`: The OTP that was generated.
+- `container`: The [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which you can use to resolve Framework and commerce resources.
+
+In the subscriber function, you resolve the Notification Module's service from the Medusa container. Then, you use its `createNotifications` method to send the OTP to the user.
+
+Under the hood, the Notification Module's service delegates the sending to the Notification Module Provider of the `sms` channel, which is the Twilio Notification Module Provider in this case.
+
+The `createNotifications` method accepts an object with the following properties:
+
+- `to`: The phone number to send the notification to. You use the phone number from the event's data payload.
+- `channel`: The channel to use to send the notification, which is `sms`.
+- `template`: The template to use for the notification content, which is `otp-template`.
+- `data`: An object containing the data to use in the template. You pass the OTP to the template.
+
+### j. Test it Out
+
+To test out the Twilio Notification Module Provider, you can follow the steps in the [Test Out Phone Authentication](#test-out-phone-authentication) section.
+
+After you authenticate the customer, the OTP will be sent to the customer's phone number using Twilio. Then, you can use the OTP to verify the authentication and receive a JWT token.
+
+Alternatively, you can also test it out after customizing the Next.js Starter Storefront, which you'll do in the next step.
+
+Make sure to remove the OTP logging line in the `generateOTP` method of the Phone Authentication Module Provider's service now that you have integrated Twilio.
+
+***
+
+## Step 4: Use Phone Authentication in the Next.js Starter Storefront
+
+In this step, you'll customize the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) to allow customers to authenticate using their phone number and OTP.
+
+By default, the Next.js Starter Storefront supports email and password authentication. You'll replace it with phone authentication, but you can also keep both authentication methods if you want to.
+
+The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.
+
+So, if your Medusa application's directory is `medusa-phone-auth`, you can find the storefront by going back to the parent directory and changing to the `medusa-phone-auth-storefront` directory:
+
+```bash
+cd ../medusa-phone-auth-storefront # change based on your project name
+```
+
+### a. Install Phone Input Package
+
+To easily show a phone input where the user can enter their phone number, install the `react-phone-number-input` package:
+
+```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
+npm install react-phone-number-input
+```
+
+You'll use it in the login and registration forms to show a phone input.
+
+### b. Add Authenticate Function
+
+Before adding the forms, you'll add the functions that send requests to the Medusa API to authenticate the customer.
+
+The first one you'll add is the `authenticateWithPhone` function, which sends a request to the `/auth/customer/phone-auth` API route to authenticate the customer using their phone number.
+
+In `src/lib/data/customer.ts`, add the following function:
+
+```ts title="src/lib/data/customer.ts" badgeLabel="Storefront" badgeColor="blue"
+export const authenticateWithPhone = async (phone: string) => {
+ try {
+ const response = await sdk.auth.login("customer", "phone-auth", {
+ phone,
+ })
+
+ if (
+ typeof response === "string" ||
+ !response.location ||
+ response.location !== "otp"
+ ) {
+ throw new Error("Failed to login")
+ }
+
+ return true
+ } catch (error: any) {
+ return error.toString()
+ }
+}
+```
+
+The function accepts the phone number as a parameter.
+
+In the function, you use the [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md), which is configured within the Next.js Starter Storefront, to send a request to the `/auth/customer/phone-auth` API route. You pass the phone number in the request body.
+
+If the request doesn't return a `location` property set to `otp`, you throw an error. Otherwise, you return `true` to indicate that the request was successful.
+
+### c. add Verify OTP Function
+
+Next, you'll add the `verifyOTP` function, which sends a request to the `/auth/customer/phone-auth/callback` API route to verify the OTP.
+
+In `src/lib/data/customer.ts`, add the following function:
+
+```ts title="src/lib/data/customer.ts" badgeLabel="Storefront" badgeColor="blue" highlights={verifyOtpHighlights}
+export const verifyOtp = async ({
+ otp,
+ phone,
+}: {
+ otp: string
+ phone: string
+}) => {
+ try {
+ const token = await sdk.auth.callback("customer", "phone-auth", {
+ phone,
+ otp,
+ })
+
+ await setAuthToken(token)
+
+ const customerCacheTag = await getCacheTag("customers")
+ revalidateTag(customerCacheTag)
+
+ await transferCart()
+
+ return true
+ } catch (e: any) {
+ return e.toString()
+ }
+}
+```
+
+The function accepts an object with the following properties:
+
+- `otp`: The OTP to verify.
+- `phone`: The phone number of the customer.
+
+In the function, you use the JS SDK to send a request to the `/auth/customer/phone-auth/callback` API route. You pass the phone number and OTP in the request body.
+
+If the request is successful and you receive a token, you set the token in the cookies and refresh the customer cache. This ensures that all customer-related UI is updated after logging in, such as showing the customer's profile when accessing the `/account` page.
+
+Then, you call the `transferCart` function to transfer the cart from the guest user to the authenticated customer.
+
+Finally, you return `true` to indicate that the request was successful.
+
+### d. Add Registration Function
+
+The last function you'll add is the `registerWithPhone` function, which will register the customer using their phone number.
+
+In `src/lib/data/customer.ts`, add the following function:
+
+```ts title="src/lib/data/customer.ts" badgeLabel="Storefront" badgeColor="blue" highlights={registerWithPhoneHighlights}
+export const registerWithPhone = async ({
+ firstName,
+ lastName,
+ phone,
+}: {
+ firstName: string
+ lastName: string
+ phone: string
+}) => {
+ try {
+ const { token: regToken } = await sdk.client.fetch<
+ { token: string }
+ >(`/auth/customer/phone-auth/register`, {
+ method: "POST",
+ body: {
+ phone,
+ },
+ })
+
+ await setAuthToken(regToken as string)
+ const headers = {
+ ...(await getAuthHeaders()),
+ }
+
+ const email = `${phone}@gmail.com`
+ const customerData = {
+ email,
+ first_name: firstName,
+ last_name: lastName,
+ phone,
+ }
+
+ await sdk.store.customer.create(
+ customerData,
+ {},
+ headers
+ )
+
+ return await authenticateWithPhone(phone)
+ } catch (error: any) {
+ return error.toString()
+ }
+}
+```
+
+The function accepts an object with the following properties:
+
+- `firstName`: The first name of the customer.
+- `lastName`: The last name of the customer.
+- `phone`: The phone number of the customer.
+
+In the function, you retrieve a registration token for the customer using the `/auth/customer/phone-auth/register` API route. You pass the phone number in the request body.
+
+Then, after setting the registration token in the cookies, you create a customer using the [Create Customer](https://docs.medusajs.com/api/store#customers_postcustomers) API route. You pass the following properties in the request body:
+
+- `email`: The email of the customer. You set it to the phone number with a `@gmail.com` domain.
+- `first_name`: The first name of the customer.
+- `last_name`: The last name of the customer.
+- `phone`: The phone number of the customer.
+
+Finally, you call the `authenticateWithPhone` function to authenticate the customer using their phone number. At this step, the customer would receive an OTP to login.
+
+### e. Add OTP Form
+
+Next, you'll add an OTP form that allows the customer to enter the OTP they receive after login or registration. Later, you'll reuse this form in both the login and registration pages.
+
+Create the file `src/modules/account/components/otp/index.tsx` with the following content:
+
+```tsx title="src/modules/account/components/otp/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={otpHighlights}
+"use client"
+
+import { Input } from "@medusajs/ui"
+import { useState, useRef, useEffect } from "react"
+import { authenticateWithPhone, verifyOtp } from "../../../../lib/data/customer"
+import ErrorMessage from "../../../checkout/components/error-message"
+
+type Props = {
+ phone: string
+}
+
+export const Otp = ({ phone }: Props) => {
+ const [otp, setOtp] = useState("")
+ const [error, setError] = useState("")
+ const [isLoading, setIsLoading] = useState(false)
+ const [countdown, setCountdown] = useState(60)
+ const inputRefs = useRef<(HTMLInputElement | null)[]>([])
+
+ const handleSubmit = async () => {
+ setIsLoading(true)
+ const response = await verifyOtp({
+ otp,
+ phone,
+ })
+ setOtp("")
+ setIsLoading(false)
+
+ if (typeof response === "string") {
+ setError(response)
+ }
+ }
+
+ const handleResend = async () => {
+ authenticateWithPhone(phone)
+ setCountdown(60)
+ }
+
+ const handlePaste = (e: React.ClipboardEvent) => {
+ e.preventDefault()
+ const pastedData = e.clipboardData.getData("text")
+ const numericValue = pastedData.replace(/\D/g, "").slice(0, 6)
+
+ if (numericValue) {
+ setOtp(numericValue)
+ // Focus the next empty input after pasted content
+ const nextEmptyIndex = Math.min(numericValue.length, 5)
+ inputRefs.current[nextEmptyIndex]?.focus()
+ }
+ }
+
+ // TODO add use effects
+}
+```
+
+You create an `Otp` component that accepts the phone number as a prop.
+
+In the component, you define the following state variables:
+
+- `otp`: The OTP entered by the customer.
+- `error`: The error message to show if the OTP verification fails.
+- `isLoading`: A boolean indicating whether the OTP verification is in progress.
+- `countdown`: The countdown timer for resending the OTP.
+- `inputRefs`: A ref to store the input elements for the OTP digits. You'll show six input elements for the OTP digits.
+
+You also define the following functions:
+
+- `handleSubmit`: This function is called when the customer submits the OTP. It calls the `verifyOtp` function to verify the OTP entered by the customer.
+- `handleResend`: This function is called when the customer clicks the "Resend OTP" button that you'll add later. It calls the `authenticateWithPhone` function to resend the OTP to the customer's phone number.
+- `handlePaste`: This function is called when the customer pastes the OTP in the input field. It improves the experience of pasting the OTP without having to enter it manually.
+
+#### Handle Variable Changes
+
+Next, you'll add `useEffect` hooks to handle changes in the state variables.
+
+Replace the `TODO` with the following:
+
+```tsx title="src/modules/account/components/otp/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={useEffectHighlights}
+useEffect(() => {
+ if (inputRefs.current[0]) {
+ inputRefs.current[0].focus()
+ }
+}, [inputRefs.current])
+
+useEffect(() => {
+ if (otp.length !== 6 || isLoading) {
+ return
+ }
+
+ handleSubmit()
+}, [otp, isLoading])
+
+useEffect(() => {
+ const timer = setInterval(() => {
+ setCountdown((prev) => {
+ return prev > 0 ? prev - 1 : 0
+ })
+ }, 1000)
+
+ return () => clearInterval(timer)
+}, [])
+
+// TODO render form
+```
+
+You add three `useEffect` hooks:
+
+1. The first one focuses the first input element when the component mounts.
+2. The second one automatically submits the OTP when the customer enters six digits.
+3. The third one adds an interval to update the countdown timer every second.
+
+### f. Render OTP Form
+
+Lastly, you'll render the OTP form with the input elements and the resend button.
+
+Replace the `TODO` with the following:
+
+```tsx title="src/modules/account/components/otp/index.tsx" badgeLabel="Storefront" badgeColor="blue"
+return (
+
+
+ Verify Phone Number
+
+
+ Enter the code sent to your phone number to login.
+
+)
+```
+
+You show six input elements for the OTP digits. When a value is entered in an input, the focus moves to the next input. In addition, when the customer presses backspace on an empty input, the focus moves to the previous input.
+
+You also show a resend button that allows the customer to resend the OTP once the countdown timer reaches zero.
+
+You now have an OTP form that you can use in both the login and registration pages.
+
+### g. Add Registration Form
+
+You'll now add a registration form that allows the customer to register using their phone number.
+
+First, in `src/modules/account/templates/login-template.tsx`, update the `LOGIN_VIEW` to the following:
+
+```tsx title="src/modules/account/templates/login-template.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["4"], ["5"]]}
+export enum LOGIN_VIEW {
+ SIGN_IN = "sign-in",
+ REGISTER = "register",
+ REGISTER_PHONE = "register-phone",
+ SIGN_IN_PHONE = "sign-in-phone",
+}
+```
+
+By default, the login template supports switching between the login and registration views for email and password authentication. With the above change, you add two new views: login and registration with phone number.
+
+Then, to add the registration form, create the file `src/modules/account/components/register-phone/index.tsx` with the following content:
+
+```tsx title="src/modules/account/components/register-phone/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={registerPhoneHighlights}
+"use client"
+
+import { useState } from "react"
+import Input from "@modules/common/components/input"
+import { LOGIN_VIEW } from "@modules/account/templates/login-template"
+import ErrorMessage from "@modules/checkout/components/error-message"
+import LocalizedClientLink from "@modules/common/components/localized-client-link"
+import { registerWithPhone } from "@lib/data/customer"
+import "react-phone-number-input/style.css"
+import PhoneInput from "react-phone-number-input"
+import { Otp } from "../otp"
+import { useParams } from "next/navigation"
+import { Button } from "@medusajs/ui"
+
+type Props = {
+ setCurrentView: (view: LOGIN_VIEW) => void
+}
+
+const RegisterPhone = ({ setCurrentView }: Props) => {
+ const [firstName, setFirstName] = useState("")
+ const [lastName, setLastName] = useState("")
+ const [phone, setPhone] = useState("")
+ const [error, setError] = useState("")
+ const [loading, setLoading] = useState(false)
+ const [enterOtp, setEnterOtp] = useState(false)
+ const { countryCode } = useParams() as { countryCode: string }
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setLoading(true)
+ const response = await registerWithPhone({
+ firstName,
+ lastName,
+ phone,
+ })
+ setLoading(false)
+ if (typeof response === "string") {
+ setError(response)
+ return
+ }
+
+ setEnterOtp(true)
+ }
+
+ if (enterOtp) {
+ return
+ }
+
+ // TODO render form
+}
+
+export default RegisterPhone
+```
+
+You create a `RegisterPhone` component that accepts a `setCurrentView` prop to switch between the login and registration views.
+
+In the component, you define the following state variables:
+
+- `firstName`, `lastName`, and `phone` to store the form inputs' values.
+- `error`: The error message to show if the registration fails.
+- `loading`: A boolean indicating whether the registration is in progress.
+- `enterOtp`: A boolean indicating whether to show the OTP form. This is enabled once the customer is registered and they need to authenticate using the OTP.
+- `countryCode`: The country code of the customer, which is retrieved from the URL parameters. You'll use this to show the phone input with a default selected country.
+
+You also define a `handleSubmit` function that handles the form submission. It calls the `registerWithPhone` function to register the customer using their phone number.
+
+If the registration is successful, you set `enterOtp` to `true` to show the OTP form. Otherwise, you set the error message.
+
+#### Render Registration Form
+
+Next, you'll render the registration form with the input fields and the submit button.
+
+Replace the `TODO` with the following:
+
+```tsx title="src/modules/account/components/register-phone/index.tsx" badgeLabel="Storefront" badgeColor="blue"
+return (
+
+
+ Become a Medusa Store Member
+
+
+ Create your Medusa Store Member profile, and get access to an enhanced
+ shopping experience.
+
+
+
+ Already a member?{" "}
+
+ .
+
+
+)
+```
+
+You render the registration form with input fields for the first name, last name, and phone number.
+
+For the phone number input, you use the `PhoneInput` component from the `react-phone-number-input` package. You set the `defaultCountry` prop to the country code retrieved from the URL parameters.
+
+You also show a submit button that calls the `handleSubmit` function when clicked, and a button to switch to the login form.
+
+#### Add to Login Template
+
+Next, you'll add the `RegisterPhone` component to the login template.
+
+In `src/modules/account/templates/login-template.tsx`, add the following import:
+
+```tsx title="src/modules/account/templates/login-template.tsx" badgeLabel="Storefront" badgeColor="blue"
+import RegisterPhone from "@modules/account/components/register-phone"
+```
+
+Then, change the `return` statement of the `LoginTemplate` component to the following:
+
+```tsx title="src/modules/account/templates/login-template.tsx" badgeLabel="Storefront" badgeColor="blue"
+return (
+
+)
+```
+
+You show the registration form when the `currentView` is set to `register-phone`. You'll also add the login form later.
+
+### h. Add Login Form
+
+Next, you'll add a login form that allows the customer to log in using their phone number.
+
+To create the form, create the file `src/modules/account/components/login-phone/index.tsx` with the following content:
+
+```tsx title="src/modules/account/components/login-phone/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={loginPhoneHighlights}
+"use client"
+
+import { authenticateWithPhone } from "@lib/data/customer"
+import { LOGIN_VIEW } from "@modules/account/templates/login-template"
+import ErrorMessage from "@modules/checkout/components/error-message"
+import { useState } from "react"
+import "react-phone-number-input/style.css"
+import PhoneInput from "react-phone-number-input"
+import { Otp } from "../otp"
+import { useParams } from "next/navigation"
+import { Button } from "@medusajs/ui"
+
+type Props = {
+ setCurrentView: (view: LOGIN_VIEW) => void
+}
+
+const LoginPhone = ({ setCurrentView }: Props) => {
+ const [phone, setPhone] = useState("")
+ const [error, setError] = useState("")
+ const [loading, setLoading] = useState(false)
+ const [enterOtp, setEnterOtp] = useState(false)
+ const { countryCode } = useParams() as { countryCode: string }
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setLoading(true)
+ const response = await authenticateWithPhone(phone)
+ setLoading(false)
+ if (typeof response === "string") {
+ setError(response)
+ return
+ }
+
+ setEnterOtp(true)
+ }
+
+ if (enterOtp) {
+ return
+ }
+
+ // TODO render form
+}
+
+export default LoginPhone
+```
+
+You create a `LoginPhone` component that accepts a `setCurrentView` prop to switch between the login and registration views.
+
+In the component, you define the following state variables:
+
+- `phone`: The phone number entered by the customer.
+- `error`: The error message to show if the login fails.
+- `loading`: A boolean indicating whether the login is in progress.
+- `enterOtp`: A boolean indicating whether to show the OTP form. This is enabled after the form is submitted.
+- `countryCode`: The country code of the customer, which is retrieved from the URL parameters. You'll use this to show the phone input with a default selected country.
+
+You also define a `handleSubmit` function that handles the form submission. It calls the `authenticateWithPhone` function to authenticate the customer using their phone number. Then, it enables `enterOtp` to show the OTP form.
+
+#### Render Login Form
+
+Next, you'll render the login form with the input field and the submit button.
+
+Replace the `TODO` with the following:
+
+```tsx title="src/modules/account/components/login-phone/index.tsx" badgeLabel="Storefront" badgeColor="blue"
+return (
+
+
Welcome back
+
+ Sign in to access an enhanced shopping experience.
+
+
+
+ Not a member?{" "}
+
+ .
+
+
+)
+```
+
+You render the login form with an input field for the phone number. You also show a submit button that calls the `handleSubmit` function when clicked.
+
+### i. Add to Login Template
+
+Next, you'll add the `LoginPhone` component to the login template.
+
+In `src/modules/account/templates/login-template.tsx`, add the following import:
+
+```tsx title="src/modules/account/templates/login-template.tsx" badgeLabel="Storefront" badgeColor="blue"
+import LoginPhone from "../components/login-phone"
+```
+
+Next, in the `LoginTemplate` component, change the default value of the `currentView` state to `LOGIN_VIEW.SIGN_IN_PHONE`:
+
+```tsx title="src/modules/account/templates/login-template.tsx" badgeLabel="Storefront" badgeColor="blue"
+const [currentView, setCurrentView] = useState(LOGIN_VIEW.SIGN_IN_PHONE)
+```
+
+This ensures the phone login form is shown by default.
+
+Finally, replace the `return` statement of the `LoginTemplate` component to the following:
+
+```tsx title="src/modules/account/templates/login-template.tsx" badgeLabel="Storefront" badgeColor="blue"
+return (
+
+)
+```
+
+You show the login form when the `currentView` is set to `sign-in-phone`.
+
+### Test it Out
+
+You can now test out the phone authentication feature in the Next.js Starter Storefront.
+
+First, start the Medusa application by running the following command in the Medusa project's directory:
+
+```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green"
+npm run dev
+```
+
+Then, start the Next.js Starter Storefront by running the following command in the storefront project's directory:
+
+```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
+npm run dev
+```
+
+Open your browser, navigate to `http://localhost:8000`, and click on the "Account" link at the top right. This will show the login form with just the phone number input.
+
+
+
+You can also switch to the registration form by clicking on the "Join" link below the login form.
+
+
+
+You can try to login with the account you created before, or register with a new one. Once successful, you'll see the OTP form to enter the OTP you received as SMS.
+
+
+
+After you enter the six digits, you'll be logged in and you'll see your profile page.
+
+
+
+***
+
+## Step 5: Disallow Phone Updates
+
+The phone authentication feature is now complete, but there are two improvements you can make:
+
+1. Show the phone number in the profile page: Currently, it shows the email address, which is a fake address you've set.
+2. Disable phone and email updates: Currently, the customer can update their phone number, which is not allowed for phone authentication.
+
+### a. Show Phone Number in Profile Page
+
+To show the phone number in the profile page instead of the email, in `src/modules/account/components/overview/index.tsx`, find the following in the `return` statement:
+
+```tsx title="src/modules/account/components/overview/index.tsx" badgeLabel="Storefront" badgeColor="blue"
+
+ {customer?.email}
+
+```
+
+And replace it with the following:
+
+```tsx title="src/modules/account/components/overview/index.tsx" badgeLabel="Storefront" badgeColor="blue"
+
+ {customer?.phone}
+
+```
+
+If you check the profile page now, you'll see the phone number instead of the email address at the top right.
+
+
+
+### b. Remove Email and Phone Fields
+
+Next, you'll remove the fields to update email and phone number from the profile page.
+
+In `src/app/[countryCode]/(main)/account/@dashboard/profile/page.tsx`, find the following lines to remove from the `return` statement:
+
+```tsx title="src/app/[countryCode]/(main)/account/@dashboard/profile/page.tsx" badgeLabel="Storefront" badgeColor="blue"
+
+ {/* ... */}
+ {/* Remove the following */}
+
+
+
+
+ {/* ... */}
+
+```
+
+If you go to your profile page and click on "Profile" in the sidebar, the email and phone number sections will be removed.
+
+
+
+### c. Disable Phone Updates in Medusa
+
+While removing the email and phone fields from the profile page prevents customers using the storefront from updating their phone number, it doesn't prevent them from updating it using Medusa's API.
+
+In this section, you'll add a [middleware](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md) to the `/store/customers/me` API route that prevents customers from updating their phone number.
+
+A middleware is a function that's executed whenever a request is sent to an API route. It's executed before the route handler, allowing you to validate requests, apply authentication guards, and more.
+
+Learn more in the [Middlewares](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md) documentation.
+
+To add a middleware in your Medusa application, create the file `src/api/middlewares.ts` with the following content:
+
+```ts title="src/api/middlewares.ts" badgeLabel="Medusa Application" badgeColor="green"
+import { defineMiddlewares } from "@medusajs/framework/http"
+
+export default defineMiddlewares({
+ routes: [
+ {
+ matcher: "/store/customers/me",
+ method: ["POST"],
+ middlewares: [
+ async (req, res, next) => {
+ const { phone } = req.body as Record
+
+ if (phone) {
+ return res.status(400).json({
+ error: "Phone number is not allowed to be updated",
+ })
+ }
+
+ next()
+ },
+ ],
+ },
+ ],
+})
+```
+
+You define middlewares using the `defineMiddlewares` function. It accepts an object having a `routes` property that holds all middlewares applied to API routes.
+
+Each object in `routes` has the following properties:
+
+- `matcher`: The API route path to apply the middleware on. You set it to `/store/customers/me`.
+- `method`: The HTTP method to apply the middleware to. You set it to `POST` so that the middleware is applied only on `POST` requests sent to the `/store/customers/me` route.
+- `middlewares`: An array of middlewares to apply on the route. You add a middleware that throws an error if the request body contains a `phone` property. This prevents customers from updating their phone number using the API.
+
+Any `POST` request to the `/store/customers/me` route will now be validated to ensure it's not updating the phone number.
+
+***
+
+## Next Steps
+
+You've now implemented phone authentication in Medusa with Twilio integration. You can further customize the phone authentication feature based on your business use case.
+
+### Authenticate Other Actor Types
+
+This tutorial focused on authenticating customers using their phone number. However, you can also authenticate other actor types, such as admin users and vendors.
+
+To do that, first, enable the `phone-auth` authentication strategy in `medusa-config.ts` for the actor types. For example:
+
+```ts title="medusa-config.ts"
+module.exports = defineConfig({
+ projectConfig: {
+ // ...
+ http: {
+ // ...
+ authMethodsPerActor: {
+ user: ["emailpass", "phone-auth"],
+ customer: ["emailpass", "phone-auth"],
+ vendor: ["emailpass", "phone-auth"],
+ },
+ },
+ },
+})
+```
+
+Then, when sending requests to the authentication API routes mentioned in the [Test Out Phone Authentication](#test-out-phone-authentication) section, replace `customer` in the API route paths with the actor type you want to authenticate:
+
+- `/auth/customer/phone-auth/register` -> `/auth/user/phone-auth/register`
+- `/auth/customer/phone-auth` -> `/auth/user/phone-auth`
+- `/auth/customer/phone-auth/callback` -> `/auth/user/phone-auth/callback`
+
+Finally, update the UI to show the phone authentication option for the actor type you want to authenticate. This depends on the UI you're using, but you can follow an approach similar to the Next.js Starter Storefront customizations.
+
+The login form of Medusa Admin can't be customized, so you'll have to build a custom admin dashboard to support phone authentication for admin users.
+
+### Learn More about Medusa
+
+If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth learning of all the concepts you've used in this guide and more.
+
+To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).
+
+### Troubleshooting
+
+If you encounter issues during your development, check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/index.html.md).
+
+### Getting Help
+
+If you encounter issues not covered in the troubleshooting guides:
+
+1. Visit the [Medusa GitHub repository](https://github.com/medusajs/medusa) to report issues or ask questions.
+2. Join the [Medusa Discord community](https://discord.gg/medusajs) for real-time support from community members.
+3. Contact the [sales team](https://medusajs.com/contact/) to get help from the Medusa team.
+
+
# Implement Product Reviews in Medusa
In this tutorial, you'll learn how to implement product reviews in Medusa.
@@ -51589,7 +52124,7 @@ In the workflow's constructor function, you:
- use `useQueryGraphStep` to retrieve the product. By setting the `options.throwIfKeyNotFound` to `true`, the step throws an error if the product doesn't exist.
- Call the `createReviewStep` step to create the review.
-`useQueryGraphStep` uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), which allows you to retrieve data across modules. For example, in the above snippet you're retrieving the cart's promotions, which are managed in the [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md), by passing `promotions.code` to the `fields` array.
+`useQueryGraphStep` uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), which allows you to retrieve data across modules. For example, in the above snippet you're retrieving the product, which is managed in the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md), by passing `id` to the `fields` array.
A workflow must return an instance of `WorkflowResponse`. The `WorkflowResponse` constructor accepts the workflow's output as a parameter, which is an object holding the created review in this case.
@@ -53051,510 +53586,6 @@ If you're new to Medusa, check out the [main documentation](https://docs.medusaj
To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).
-# Implement Quick Re-Order Functionality in Medusa
-
-In this tutorial, you'll learn how to implement a re-order functionality in Medusa.
-
-When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md) which are available out-of-the-box. The features include order-management features.
-
-The Medusa Framework facilitates building custom features that are necessary for your business use case. In this tutorial, you'll learn how to implement a re-order functionality in Medusa. This feature is useful for businesses whose customers are likely to repeat their orders, such as B2B or food delivery businesses.
-
-You can follow this guide whether you're new to Medusa or an advanced Medusa developer.
-
-## Summary
-
-By following this tutorial, you'll learn how to:
-
-- Install and set up Medusa.
-- Define the logic to re-order an order.
-- Customize the Next.js Starter Storefront to add a re-order button.
-
-
-
-- [Re-Order Repository](https://github.com/medusajs/examples/tree/main/re-order): Find the full code for this guide in this repository.
-- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1741941475/OpenApi/product-reviews_jh8ohj.yaml): Import this OpenApi Specs file into tools like Postman.
-
-***
-
-## Step 1: Install a Medusa Application
-
-### Prerequisites
-
-- [Node.js v20+](https://nodejs.org/en/download)
-- [Git CLI tool](https://git-scm.com/downloads)
-- [PostgreSQL](https://www.postgresql.org/download/)
-
-Start by installing the Medusa application on your machine with the following command:
-
-```bash
-npx create-medusa-app@latest
-```
-
-You'll first be asked for the project's name. Then, when asked whether you want to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose Yes.
-
-Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js Starter Storefront in a separate directory with the `{project-name}-storefront` name.
-
-The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).
-
-Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterwards, you can log in with the new user and explore the dashboard.
-
-Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.
-
-***
-
-## Step 2: Implement Re-Order Workflow
-
-To build custom commerce features in Medusa, you create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). A workflow is a series of queries and actions, called steps, that complete a task.
-
-By using workflows, you can track their executions' progress, define roll-back logic, and configure other advanced features. Then, you execute the workflow from other customizations, such as in an API Route.
-
-In this section, you'll implement the re-order functionality in a workflow. Later, you'll execute the workflow in a custom API route.
-
-Refer to the [Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) to learn more.
-
-The workflow will have the following steps:
-
-- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the order's details.
-- [createCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartWorkflow/index.html.md): Create a cart for the re-order.
-- [addShippingMethodToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addShippingMethodToCartWorkflow/index.html.md): Add the order's shipping method(s) to the cart.
-- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details.
-
-This workflow uses steps from Medusa's `@medusajs/medusa/core-flows` package. So, you can implement the workflow without implementing custom steps.
-
-### a. Create the Workflow
-
-To create the workflow, create the file `src/workflows/reorder.ts` with the following content:
-
-```ts title="src/workflows/reorder.ts" highlights={workflowHighlights1}
-import {
- createWorkflow,
- transform,
- WorkflowResponse,
-} from "@medusajs/framework/workflows-sdk"
-import {
- addShippingMethodToCartWorkflow,
- createCartWorkflow,
- useQueryGraphStep,
-} from "@medusajs/medusa/core-flows"
-
-type ReorderWorkflowInput = {
- order_id: string
-}
-
-export const reorderWorkflow = createWorkflow(
- "reorder",
- ({ order_id }: ReorderWorkflowInput) => {
- // @ts-ignore
- const { data: orders } = useQueryGraphStep({
- entity: "order",
- fields: [
- "*",
- "items.*",
- "shipping_address.*",
- "billing_address.*",
- "region.*",
- "sales_channel.*",
- "shipping_methods.*",
- "customer.*",
- ],
- filters: {
- id: order_id,
- },
- })
-
- // TODO create a cart with the order's items
- }
-)
-```
-
-You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter.
-
-It accepts as a second parameter a constructor function, which is the workflow's implementation. The function can accept input, which in this case is an object holding the ID of the order to re-order.
-
-In the workflow's constructor function, so far you use the `useQueryGraphStep` step to retrieve the order's details. This step uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) under the hood, which allows you to query data across [modules](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md).
-
-Refer to the [Query documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to learn more about how to use it.
-
-### b. Create a Cart
-
-Next, you need to create a cart using the old order's details. You can use the `createCartWorkflow` step to create a cart, but you first need to prepare its input data.
-
-Replace the `TODO` in the workflow with the following:
-
-```ts title="src/workflows/reorder.ts" highlights={workflowHighlights2}
-const createInput = transform({
- orders,
-}, (data) => {
- return {
- region_id: data.orders[0].region_id!,
- sales_channel_id: data.orders[0].sales_channel_id!,
- customer_id: data.orders[0].customer_id!,
- email: data.orders[0].email!,
- billing_address: {
- first_name: data.orders[0].billing_address?.first_name!,
- last_name: data.orders[0].billing_address?.last_name!,
- address_1: data.orders[0].billing_address?.address_1!,
- city: data.orders[0].billing_address?.city!,
- country_code: data.orders[0].billing_address?.country_code!,
- province: data.orders[0].billing_address?.province!,
- postal_code: data.orders[0].billing_address?.postal_code!,
- phone: data.orders[0].billing_address?.phone!,
- },
- shipping_address: {
- first_name: data.orders[0].shipping_address?.first_name!,
- last_name: data.orders[0].shipping_address?.last_name!,
- address_1: data.orders[0].shipping_address?.address_1!,
- city: data.orders[0].shipping_address?.city!,
- country_code: data.orders[0].shipping_address?.country_code!,
- province: data.orders[0].shipping_address?.province!,
- postal_code: data.orders[0].shipping_address?.postal_code!,
- phone: data.orders[0].shipping_address?.phone!,
- },
- items: data.orders[0].items?.map((item) => ({
- variant_id: item?.variant_id!,
- quantity: item?.quantity!,
- unit_price: item?.unit_price!,
- })),
- }
-})
-
-const { id: cart_id } = createCartWorkflow.runAsStep({
- input: createInput,
-})
-
-// TODO add the shipping method to the cart
-```
-
-Data manipulation is not allowed in a workflow, as Medusa stores its definition before executing it. Instead, you can use `transform` from the Workflows SDK to manipulate the data.
-
-Learn more about why you can't manipulate data in a workflow and the `transform` function in the [Data Manipulation in Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md).
-
-`transform` accepts the following parameters:
-
-1. The data to use in the transformation function.
-2. A transformation function that accepts the data from the first parameter and returns the transformed data.
-
-In the above code snippet, you use `transform` to create the input for the `createCartWorkflow` step. The input is an object that holds the cart's details, including its items, shipping and billing addresses, and more.
-
-Learn about other input parameters you can pass in the [createCartWorkflow reference](https://docs.medusajs.com/references/medusa-workflows/createCartWorkflow/index.html.md).
-
-After that, you execute the `createCartWorkflow` passing it the transformed input. The workflow returns the cart's details, including its ID.
-
-### c. Add Shipping Methods
-
-Next, you need to add the order's shipping method(s) to the cart. This saves the customer from having to select a shipping method again.
-
-You can use the `addShippingMethodToCartWorkflow` step to add the shipping method(s) to the cart.
-
-Replace the `TODO` in the workflow with the following:
-
-```ts title="src/workflows/reorder.ts" highlights={workflowHighlights3}
-const addShippingMethodToCartInput = transform({
- cart_id,
- orders,
-}, (data) => {
- return {
- cart_id: data.cart_id,
- options: data.orders[0].shipping_methods?.map((method) => ({
- id: method?.shipping_option_id!,
- data: method?.data!,
- })) ?? [],
- }
-})
-
-addShippingMethodToCartWorkflow.runAsStep({
- input: addShippingMethodToCartInput,
-})
-
-// TODO retrieve and return the cart's details
-```
-
-Again, you use `transform` to prepare the input for the `addShippingMethodToCartWorkflow`. The input includes the cart's ID and the shipping method(s) to add to the cart.
-
-Then, you execute the `addShippingMethodToCartWorkflow` to add the shipping method(s) to the cart.
-
-### d. Retrieve and Return the Cart's Details
-
-Finally, you need to retrieve the cart's details and return them as the workflow's output.
-
-Replace the `TODO` in the workflow with the following:
-
-```ts title="src/workflows/reorder.ts" highlights={workflowHighlights4}
-// @ts-ignore
-const { data: carts } = useQueryGraphStep({
- entity: "cart",
- fields: [
- "*",
- "items.*",
- "shipping_methods.*",
- "shipping_address.*",
- "billing_address.*",
- "region.*",
- "sales_channel.*",
- "promotions.*",
- "currency_code",
- "subtotal",
- "item_total",
- "total",
- "item_subtotal",
- "shipping_subtotal",
- "customer.*",
- "payment_collection.*",
-
- ],
- filters: {
- id: cart_id,
- },
-}).config({ name: "retrieve-cart" })
-
-return new WorkflowResponse(carts[0])
-```
-
-You execute the `useQueryGraphStep` again to retrieve the cart's details. Since you're re-using a step, you have to rename it using the `config` method.
-
-Finally, you return the cart's details. A workflow must return an instance of `WorkflowResponse`.
-
-The `WorkflowResponse` constructor accepts the workflow's output as a parameter, which is the cart's details in this case.
-
-In the next step, you'll create an API route that exposes the re-order functionality.
-
-***
-
-## Step 3: Create Re-Order API Route
-
-Now that you have the logic to re-order, you need to expose it so that frontend clients, such as a storefront, can use it. You do this by creating an [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md).
-
-An API Route is an endpoint that exposes commerce features to external applications and clients, such as storefronts. You'll create an API route at the path `/store/customers/me/orders/:id` that executes the workflow from the previous step.
-
-Refer to the [API Routes documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) to learn more.
-
-An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`.
-
-So, create the file `src/api/store/customers/me/orders/[id]/route.ts` with the following content:
-
-```ts title="src/api/store/customers/me/orders/[id]/route.ts"
-import {
- AuthenticatedMedusaRequest,
- MedusaResponse,
-} from "@medusajs/framework/http"
-import { reorderWorkflow } from "../../../../../../workflows/reorder"
-
-export async function POST(
- req: AuthenticatedMedusaRequest,
- res: MedusaResponse
-) {
- const { id } = req.params
-
- const { result } = await reorderWorkflow(req.scope).run({
- input: {
- order_id: id,
- },
- })
-
- return res.json({
- cart: result,
- })
-}
-```
-
-Since you export a `POST` route handler function, you expose a `POST` API route at `/store/customers/me/orders/:id`.
-
-API routes that start with `/store/customers/me` are protected by default, meaning that only authenticated customers can access them. Learn more in the [Protected API Routes documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/protected-routes/index.html.md).
-
-The route handler function accepts two parameters:
-
-1. A request object with details and context on the request, such as path parameters or authenticated customer details.
-2. A response object to manipulate and send the response.
-
-In the route handler function, you execute the `reorderWorkflow`. To execute a workflow, you:
-
-- Invoke it, passing it the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) available in the `req.scope` property.
- - The Medusa container is a registry of Framework and commerce resources that you can resolve and use in your customizations.
-- Call the `run` method, passing it an object with the workflow's input.
-
-You pass the order ID from the request's path parameters as the workflow's input. Finally, you return the created cart's details in the response.
-
-You'll test out this API route after you customize the Next.js Starter Storefront.
-
-***
-
-## Step 4: Customize the Next.js Starter Storefront
-
-In this step, you'll customize the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) to add a re-order button. You installed the Next.js Starter Storefront in the first step with the Medusa application, but you can also install it separately as explained in the [Next.js Starter Storefront documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md).
-
-The Next.js Starter Storefront provides rich commerce features and a sleek design. You can use it as-is or build on top of it to tailor it for your business's unique use case, design, and customer experience.
-
-The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.
-
-So, if your Medusa application's directory is `medusa-reorder`, you can find the storefront by going back to the parent directory and changing to the `medusa-reorder-storefront` directory:
-
-```bash
-cd ../medusa-reorder-storefront # change based on your project name
-```
-
-To add the re-order button, you will:
-
-- Add a server function that re-orders an order using the API route from the previous step.
-- Add a button to the order details page that calls the server function.
-
-### a. Add the Server Function
-
-You'll add the server function for the re-order functionality in the `src/lib/data/orders.ts` file.
-
-First, add the following import statement to the top of the file:
-
-```ts title="src/lib/data/orders.ts" badgeLabel="Storefront" badgeColor="blue"
-import { setCartId } from "./cookies"
-```
-
-Then, add the function at the end of the file:
-
-```ts title="src/lib/data/orders.ts" badgeLabel="Storefront" badgeColor="blue"
-export const reorder = async (id: string) => {
- const headers = await getAuthHeaders()
-
- const { cart } = await sdk.client.fetch(
- `/store/customers/me/orders/${id}`,
- {
- method: "POST",
- headers,
- }
- )
-
- await setCartId(cart.id)
-
- return cart
-}
-```
-
-You add a function that accepts the order ID as a parameter.
-
-The function uses the `client.fetch` method of the [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md) to send a request to the API route you created in the previous step.
-
-The JS SDK is already configured in the Next.js Starter Storefront. Refer to the [JS SDK documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md) to learn more about it.
-
-Once the request succeeds, you use the `setCartId` function that's defined in the storefront to set the cart ID in a cookie. This ensures the cart is used across the storefront.
-
-Finally, you return the cart's details.
-
-### b. Add the Re-Order Button Component
-
-Next, you'll add the component that shows the re-order button. You'll later add the component to the order details page.
-
-To create the component, create the file `src/modules/order/components/reorder-action/index.tsx` with the following content:
-
-```tsx title="src/modules/order/components/reorder-action/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={componentHighlights}
-import { Button, toast } from "@medusajs/ui"
-import { reorder } from "../../../../lib/data/orders"
-import { useState } from "react"
-import { useRouter } from "next/navigation"
-
-type ReorderActionProps = {
- orderId: string
-}
-
-export default function ReorderAction({ orderId }: ReorderActionProps) {
- const [isLoading, setIsLoading] = useState(false)
- const router = useRouter()
-
- const handleReorder = async () => {
- setIsLoading(true)
- try {
- const cart = await reorder(orderId)
-
- setIsLoading(false)
- toast.success("Prepared cart to reorder. Proceeding to checkout...")
- router.push(`/${cart.shipping_address!.country_code}/checkout?step=payment`)
- } catch (error) {
- setIsLoading(false)
- toast.error(`Error reordering: ${error}`)
- }
- }
-
- return (
-
- )
-}
-```
-
-You create a `ReorderAction` component that accepts the order ID as a prop.
-
-In the component, you render a button that, when clicked, calls a `handleReorder` function. The function calls the `reorder` function you created in the previous step to re-order the order.
-
-If the re-order succeeds, you redirect the user to the payment step of the checkout page. If it fails, you show an error message.
-
-### c. Show Re-Order Button on Order Details Page
-
-Finally, you'll show the `ReorderAction` component on the order details page.
-
-In `src/modules/order/templates/order-details-template.tsx`, add the following import statement to the top of the file:
-
-```tsx title="src/modules/order/templates/order-details-template.tsx" badgeLabel="Storefront" badgeColor="blue"
-import ReorderAction from "../components/reorder-action"
-```
-
-Then, in the return statement of the `OrderDetailsTemplate` component, find the `OrderDetails` component and add the `ReorderAction` component below it:
-
-```tsx title="src/modules/order/templates/order-details-template.tsx" badgeLabel="Storefront" badgeColor="blue"
-
-```
-
-The re-order button will now be shown on the order details page.
-
-### Test it Out
-
-You'll now test out the re-order functionality.
-
-First, to start the Medusa application, run the following command in the Medusa application's directory:
-
-```bash npm2yarn badgeLabel="Medusa application" badgeColor="green"
-npm run dev
-```
-
-Then, in the Next.js Starter Storefront directory, run the following command to start the storefront:
-
-```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
-npm run dev
-```
-
-The storefront will be running at `http://localhost:8000`. Open it in your browser.
-
-To test out the re-order functionality:
-
-- Create an account in the storefront.
-- Add a product to the cart and complete the checkout process to place an order.
-- Go to Account -> Orders, and click on the "See details" button.
-
-
-
-On the order's details page, you'll find a "Reorder" button.
-
-
-
-When you click on the button, a new cart will be created with the order's details, and you'll be redirected to the checkout page where you can complete the purchase.
-
-
-
-***
-
-## Next Steps
-
-You now have a re-order functionality in your Medusa application and Next.js Starter Storefront. You can expand more on this feature based on your use case.
-
-For example, you can add quick orders on the storefront's homepage, allowing customers to quickly re-order their last orders.
-
-If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth learning of all the concepts you've used in this guide and more.
-
-To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).
-
-
# 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.
@@ -54869,6 +54900,2775 @@ 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 Localization in Medusa by Integrating Contentful
+
+In this tutorial, you'll learn how to localize your Medusa store's data with Contentful.
+
+When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. While Medusa provides features essential for internationalization, such as support for multiple [regions](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/region/index.html.md) and [currencies](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/currency/index.html.md), it doesn't provide content localization.
+
+However, Medusa's architecture supports the integration of third-party services to provide additional features, such as data localization. One service you can integrate is [Contentful](https://www.contentful.com/), a headless content management system (CMS) that allows you to manage and deliver content across multiple channels.
+
+## Summary
+
+By following this tutorial, you'll learn how to:
+
+- Install and set up Medusa.
+- Integrate Contentful with Medusa.
+- Create content types in Contentful for Medusa models.
+- Trigger syncing products and related data to Contentful when:
+ - A product is created.
+ - The admin user triggers syncing the products.
+- Customize the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) to fetch localized data from Contentful through Medusa.
+- Listen to webhook events in Contentful to update Medusa's data accordingly.
+
+You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer.
+
+
+
+- [Tutorial Repository](https://github.com/medusajs/examples/tree/main/localization-contentful): Find the full code for this guide in this repository.
+- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1744790686/OpenApi/Contentful_jysc07.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
+```
+
+First, you'll be asked for the project's name. Then, when prompted about installing the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose "Yes."
+
+Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name and the Next.js Starter Storefront in a separate directory named `{project-name}-storefront`.
+
+The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).
+
+Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterwards, you can log in with the new user and explore the dashboard.
+
+Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.
+
+***
+
+## Step 2: Create Contentful Module
+
+To integrate third-party services into Medusa, you create a module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a reusable package that provides 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 module that provides the necessary functionalities to integrate Contentful with Medusa.
+
+Refer to the [Modules](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) documentation to learn more about modules and their structure.
+
+### Install Contentful SDKs
+
+Before building the module, you need to install Contentful's management and delivery JS SDKs. So, run the following command in the Medusa application's directory:
+
+```bash npm2yarn
+npm install contentful contentful-management
+```
+
+Where `contentful` is the delivery SDK and `contentful-management` is the management SDK.
+
+### Create Module Directory
+
+A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/contentful`.
+
+### Create Loader
+
+When the Medusa application starts, you want to establish a connection to Contentful, then create the necessary content types if they don't exist in Contentful.
+
+A module can specify a task to run on the Medusa application's startup using [loaders](https://docs.medusajs.com/docs/learn/fundamentals/modules/loaders/index.html.md). A loader is an asynchronous function that a module exports. Then, when the Medusa application starts, it runs the loader. The loader can be used to perform one-time tasks such as connecting to a database, creating content types, or initializing data.
+
+Refer to the [Loaders](https://docs.medusajs.com/docs/learn/fundamentals/modules/loaders/index.html.md) documentation to learn more about how loaders work and when to use them.
+
+Loaders are created in a TypeScript or JavaScript file under the `loaders` directory of a module. So, create the file `src/modules/contentful/loader/create-content-models.ts` with the following content:
+
+```ts title="src/modules/contentful/loader/create-content-models.ts" highlights={loaderHighlights}
+import { LoaderOptions } from "@medusajs/framework/types"
+import { asValue } from "awilix"
+import { createClient } from "contentful-management"
+import { MedusaError } from "@medusajs/framework/utils"
+
+const { createClient: createDeliveryClient } = require("contentful")
+
+export type ModuleOptions = {
+ management_access_token: string
+ delivery_token: string
+ space_id: string
+ environment: string
+ default_locale?: string
+}
+
+export default async function syncContentModelsLoader({
+ container,
+ options,
+}: LoaderOptions) {
+ if (
+ !options?.management_access_token || !options?.delivery_token ||
+ !options?.space_id || !options?.environment
+ ) {
+ throw new MedusaError(
+ MedusaError.Types.INVALID_DATA,
+ "Contentful access token, space ID and environment are required"
+ )
+ }
+
+ const logger = container.resolve("logger")
+
+ try {
+ const managementClient = createClient({
+ accessToken: options.management_access_token,
+ }, {
+ type: "plain",
+ defaults: {
+ spaceId: options.space_id,
+ environmentId: options.environment,
+ },
+ })
+
+ const deliveryClient = createDeliveryClient({
+ accessToken: options.delivery_token,
+ space: options.space_id,
+ environment: options.environment,
+ })
+
+
+ // TODO try to create content types
+
+ } catch (error) {
+ logger.error(
+ `Failed to connect to Contentful: ${error}`
+ )
+ throw error
+ }
+}
+```
+
+The loader file exports an asynchronous function that accepts an object having the following properties:
+
+- `container`: The [Module container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md), which is a registry of resources available to the module. You can use it to resolve or register resources in the module's container.
+- `options`: An object of options passed to the module. These options are useful to pass secrets or options that may change per environment. You'll learn how to pass these options later.
+ - The Contentful Module expects the options to include the Contentful tokens for the management and delivery APIs, the space ID, environment, and optionally the default locale to use.
+
+In the loader function, you validate the options passed to the module, and throw an error if they're invalid. Then, you resolve from the Module's container the [Logger](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md) used to log messages in the terminal.
+
+Finally, you create clients for Contentful's management and delivery APIs, passing them the necessary module's options. If the connection fails, an error is thrown, which is handled in the `catch` block.
+
+#### Create Content Types
+
+In the loader, you need to create content types in Contentful if they don't already exist.
+
+In this tutorial, you'll only create content types for a product and its variants and options. However, you can create content types for other data models, such as categories or collections, by following the same approach.
+
+You can learn more about the product-related data models, which the content types are based on, in the [Product Module's Data Models](https://docs.medusajs.com/references/product/models/index.html.md) reference.
+
+To create the content type for products, replace the `TODO` in the loader with the following:
+
+```ts title="src/modules/contentful/loader/create-content-models.ts"
+// Try to create the product content type
+try {
+ await managementClient.contentType.get({
+ contentTypeId: "product",
+ })
+} catch (error) {
+ const productContentType = await managementClient.contentType.createWithId({
+ contentTypeId: "product",
+ }, {
+ name: "Product",
+ description: "Product content type synced from Medusa",
+ displayField: "title",
+ fields: [
+ {
+ id: "title",
+ name: "Title",
+ type: "Symbol",
+ required: true,
+ localized: true,
+ },
+ {
+ id: "handle",
+ name: "Handle",
+ type: "Symbol",
+ required: true,
+ localized: false,
+ },
+ {
+ id: "medusaId",
+ name: "Medusa ID",
+ type: "Symbol",
+ required: true,
+ localized: false,
+ },
+ {
+ type: "RichText",
+ name: "description",
+ id: "description",
+ validations: [
+ {
+ enabledMarks: [
+ "bold",
+ "italic",
+ "underline",
+ "code",
+ "superscript",
+ "subscript",
+ "strikethrough",
+ ],
+ },
+ {
+ enabledNodeTypes: [
+ "heading-1",
+ "heading-2",
+ "heading-3",
+ "heading-4",
+ "heading-5",
+ "heading-6",
+ "ordered-list",
+ "unordered-list",
+ "hr",
+ "blockquote",
+ "embedded-entry-block",
+ "embedded-asset-block",
+ "table",
+ "asset-hyperlink",
+ "embedded-entry-inline",
+ "entry-hyperlink",
+ "hyperlink",
+ ],
+ },
+ {
+ nodes: {},
+ },
+ ],
+ localized: true,
+ required: true,
+ },
+ {
+ type: "Symbol",
+ name: "subtitle",
+ id: "subtitle",
+ localized: true,
+ required: false,
+ validations: [],
+ },
+ {
+ type: "Array",
+ items: {
+ type: "Link",
+ linkType: "Asset",
+ validations: [],
+ },
+ name: "images",
+ id: "images",
+ localized: true,
+ required: false,
+ validations: [],
+ },
+ {
+ id: "productVariants",
+ name: "Product Variants",
+ type: "Array",
+ localized: false,
+ required: false,
+ items: {
+ type: "Link",
+ validations: [
+ {
+ linkContentType: ["productVariant"],
+ },
+ ],
+ linkType: "Entry",
+ },
+ disabled: false,
+ omitted: false,
+ },
+ {
+ id: "productOptions",
+ name: "Product Options",
+ type: "Array",
+ localized: false,
+ required: false,
+ items: {
+ type: "Link",
+ validations: [
+ {
+ linkContentType: ["productOption"],
+ },
+ ],
+ linkType: "Entry",
+ },
+ disabled: false,
+ omitted: false,
+ },
+ ],
+ })
+
+ await managementClient.contentType.publish({
+ contentTypeId: "product",
+ }, productContentType)
+}
+
+// TODO create product variant content type
+```
+
+In the above snippet, you first try to retrieve the product content type using Contentful's Management APIs. If the content type doesn't exist, an error is thrown, which you handle in the `catch` block.
+
+In the `catch` block, you create the product content type with the following fields:
+
+- `title`: The product's title, which is a localized field.
+- `handle`: The product's handle, which is used to create a human-readable URL for the product in the storefront.
+- `medusaId`: The product's ID in Medusa, which is a non-localized field. You'll store in this field the ID of the product in Medusa.
+- `description`: The product's description, which is a localized rich-text field.
+- `subtitle`: The product's subtitle, which is a localized field.
+- `images`: The product's images, which is a localized array of assets in Contentful.
+- `productVariants`: The product's variants, which is an array that references content of the `productVariant` content type.
+- `productOptions`: The product's options, which is an array that references content of the `productOption` content type.
+
+Next, you'll create the `productVariant` content type that represents a product's variant. A variant is a combination of the product's options that customers can purchase. For example, a "red" shirt is a variant whose color option is `red`.
+
+To create the variant content type, replace the new `TODO` with the following:
+
+```ts title="src/modules/contentful/loader/create-content-models.ts"
+// Try to create the product variant content type
+try {
+ await managementClient.contentType.get({
+ contentTypeId: "productVariant",
+ })
+} catch (error) {
+ const productVariantContentType = await managementClient.contentType.createWithId({
+ contentTypeId: "productVariant",
+ }, {
+ name: "Product Variant",
+ description: "Product variant content type synced from Medusa",
+ displayField: "title",
+ fields: [
+ {
+ id: "title",
+ name: "Title",
+ type: "Symbol",
+ required: true,
+ localized: true,
+ },
+ {
+ id: "product",
+ name: "Product",
+ type: "Link",
+ required: true,
+ localized: false,
+ validations: [
+ {
+ linkContentType: ["product"],
+ },
+ ],
+ disabled: false,
+ omitted: false,
+ linkType: "Entry",
+ },
+ {
+ id: "medusaId",
+ name: "Medusa ID",
+ type: "Symbol",
+ required: true,
+ localized: false,
+ },
+ {
+ id: "productOptionValues",
+ name: "Product Option Values",
+ type: "Array",
+ localized: false,
+ required: false,
+ items: {
+ type: "Link",
+ validations: [
+ {
+ linkContentType: ["productOptionValue"],
+ },
+ ],
+ linkType: "Entry",
+ },
+ disabled: false,
+ omitted: false,
+ },
+ ],
+ })
+
+ await managementClient.contentType.publish({
+ contentTypeId: "productVariant",
+ }, productVariantContentType)
+}
+
+// TODO create product option content type
+```
+
+In the above snippet, you create the `productVariant` content type with the following fields:
+
+- `title`: The product variant's title, which is a localized field.
+- `product`: References the `product` content type, which is the product that the variant belongs to.
+- `medusaId`: The product variant's ID in Medusa, which is a non-localized field. You'll store in this field the ID of the variant in Medusa.
+- `productOptionValues`: The product variant's option values, which is an array that references content of the `productOptionValue` content type.
+
+Then, you'll create the `productOption` content type that represents a product's option, like size or color. Replace the new `TODO` with the following:
+
+```ts title="src/modules/contentful/loader/create-content-models.ts"
+// Try to create the product option content type
+try {
+ await managementClient.contentType.get({
+ contentTypeId: "productOption",
+ })
+} catch (error) {
+ const productOptionContentType = await managementClient.contentType.createWithId({
+ contentTypeId: "productOption",
+ }, {
+ name: "Product Option",
+ description: "Product option content type synced from Medusa",
+ displayField: "title",
+ fields: [
+ {
+ id: "title",
+ name: "Title",
+ type: "Symbol",
+ required: true,
+ localized: true,
+ },
+ {
+ id: "product",
+ name: "Product",
+ type: "Link",
+ required: true,
+ localized: false,
+ validations: [
+ {
+ linkContentType: ["product"],
+ },
+ ],
+ disabled: false,
+ omitted: false,
+ linkType: "Entry",
+ },
+ {
+ id: "medusaId",
+ name: "Medusa ID",
+ type: "Symbol",
+ required: true,
+ localized: false,
+ },
+ {
+ id: "values",
+ name: "Values",
+ type: "Array",
+ required: false,
+ localized: false,
+ items: {
+ type: "Link",
+ validations: [
+ {
+ linkContentType: ["productOptionValue"],
+ },
+ ],
+ linkType: "Entry",
+ },
+ disabled: false,
+ omitted: false,
+ },
+ ],
+ })
+
+ await managementClient.contentType.publish({
+ contentTypeId: "productOption",
+ }, productOptionContentType)
+}
+
+// TODO create product option value content type
+```
+
+In the above snippet, you create the `productOption` content type with the following fields:
+
+- `title`: The product option's title, which is a localized field.
+- `product`: References the `product` content type, which is the product that the option belongs to.
+- `medusaId`: The product option's ID in Medusa, which is a non-localized field. You'll store in this field the ID of the option in Medusa.
+- `values`: The product option's values, which is an array that references content of the `productOptionValue` content type.
+
+Finally, you'll create the `productOptionValue` content type that represents a product's option value, like "red" or "blue" for the color option. A variant references option values.
+
+To create the option value content type, replace the new `TODO` with the following:
+
+```ts title="src/modules/contentful/loader/create-content-models.ts"
+// Try to create the product option value content type
+try {
+ await managementClient.contentType.get({
+ contentTypeId: "productOptionValue",
+ })
+} catch (error) {
+ const productOptionValueContentType = await managementClient.contentType.createWithId({
+ contentTypeId: "productOptionValue",
+ }, {
+ name: "Product Option Value",
+ description: "Product option value content type synced from Medusa",
+ displayField: "value",
+ fields: [
+ {
+ id: "value",
+ name: "Value",
+ type: "Symbol",
+ required: true,
+ localized: true,
+ },
+ {
+ id: "medusaId",
+ name: "Medusa ID",
+ type: "Symbol",
+ required: true,
+ localized: false,
+ },
+ ],
+})
+
+await managementClient.contentType.publish({
+ contentTypeId: "productOptionValue",
+ }, productOptionValueContentType)
+}
+
+// TODO register clients in container
+```
+
+In the above snippet, you create the `productOptionValue` content type with the following fields:
+
+- `value`: The product option value, which is a localized field.
+- `medusaId`: The product option value's ID in Medusa, which is a non-localized field. You'll store in this field the ID of the option value in Medusa.
+
+You've now created all the necessary content types to localize products.
+
+### Register Clients in the Container
+
+The last step in the loader is to register the Contentful management and delivery clients in the module's container. This will allow you to resolve and use them in the module's service, which you'll create next.
+
+To register resources in the container, you can use its `register` method, which accepts an object containing key-value pairs. The keys are the names of the resources in the container, and the values are the resources themselves.
+
+To register the management and delivery clients, replace the last `TODO` in the loader with the following:
+
+```ts title="src/modules/contentful/loader/create-content-models.ts"
+container.register({
+ contentfulManagementClient: asValue(managementClient),
+ contentfulDeliveryClient: asValue(deliveryClient),
+})
+
+logger.info("Connected to Contentful")
+```
+
+Now, you can resolve the management and delivery clients from the module's container using the keys `contentfulManagementClient` and `contentfulDeliveryClient`, respectively.
+
+### Create Service
+
+You define a module's functionality 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 perform actions with a third-party service.
+
+In this section, you'll create the Contentful Module's service that can be used to retrieve content from Contentful, create content, and more.
+
+To create the service, create the file `src/modules/contenful/service.ts` with the following content:
+
+```ts title="src/modules/contentful/service.ts" highlights={serviceHighlights}
+import { ModuleOptions } from "./loader/create-content-models"
+import { PlainClientAPI } from "contentful-management"
+
+type InjectedDependencies = {
+ contentfulManagementClient: PlainClientAPI;
+ contentfulDeliveryClient: any;
+}
+
+export default class ContentfulModuleService {
+ private managementClient: PlainClientAPI
+ private deliveryClient: any
+ private options: ModuleOptions
+
+ constructor(
+ {
+ contentfulManagementClient,
+ contentfulDeliveryClient,
+ }: InjectedDependencies,
+ options: ModuleOptions
+ ) {
+ this.managementClient = contentfulManagementClient
+ this.deliveryClient = contentfulDeliveryClient
+ this.options = {
+ ...options,
+ default_locale: options.default_locale || "en-US",
+ }
+ }
+
+ // TODO add methods
+}
+```
+
+You export a class that will be the Contentful Module's main service. In the class, you define properties for the Contentful clients and options passed to the module.
+
+You also add a constructor to the class. A service's constructor accepts the following params:
+
+1. The module's container, which you can use to resolve resources. You use it to resolve the Contentful clients you previously registered in the loader.
+2. The options passed to the module.
+
+In the constructor, you assign the clients and options to the class properties. You also set the default locale to `en-US` if it's not provided in the module's options.
+
+Since the loader is executed on application start-up, if an error occurs while connecting to Contentful, the module will not be registered and the service will not be executed. So, in the service, you're guaranteed that the clients are registered in the container and have successful connection to Contentful.
+
+As you implement the syncing and content retrieval features later, you'll add the necessary methods for them.
+
+### Export Module Definition
+
+The final piece to a module is its definition, which you export in an `index.ts` file at the module's root directory. This definition tells Medusa the name of the module, its service, and optionally its loaders.
+
+To create the module's definition, create the file `src/modules/contentful/index.ts` with the following content:
+
+```ts title="src/modules/contentful/index.ts" highlights={moduleHighlights}
+import { Module } from "@medusajs/framework/utils"
+import ContentfulModuleService from "./service"
+import createContentModelsLoader from "./loader/create-content-models"
+
+export const CONTENTFUL_MODULE = "contentful"
+
+export default Module(CONTENTFUL_MODULE, {
+ service: ContentfulModuleService,
+ loaders: [
+ createContentModelsLoader,
+ ],
+})
+```
+
+You use `Module` from the Modules SDK to create the module's definition. It accepts two parameters:
+
+1. The module's name, which is `contentful`.
+2. An object with a required property `service` indicating the module's service. You also pass the loader you created to ensure it's executed when the application starts.
+
+Aside from the module definition, you export the module's name as `CONTENTFUL_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/contentful",
+ options: {
+ management_access_token: process.env.CONTENTFUL_MANAGEMNT_ACCESS_TOKEN,
+ delivery_token: process.env.CONTENTFUL_DELIVERY_TOKEN,
+ space_id: process.env.CONTENTFUL_SPACE_ID,
+ environment: process.env.CONTENTFUL_ENVIRONMENT,
+ default_locale: "en-US",
+ },
+ },
+ ],
+})
+```
+
+Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package's name.
+
+You also pass an `options` property with the module's options, including the Contentful's tokens for the management and delivery APIs, the Contentful's space ID, environment, and default locale.
+
+### Note about Locales
+
+By default, your Contentful space will have one locale (for example, `en-US`). You can add locales as explained in the [Contentful documentation](https://www.contentful.com/help/localization/manage-locales/).
+
+When you add a locale, make sure to:
+
+- Set the fallback locale to the default locale (for example, `en-US`). This ensure that values are retrieved in the default locale if values for the requested locale are not available.
+- Allow the required fields to be empty for the locale. Otherwise, you'll have to specify the values for the localized fields in each locale when you create the products later.
+
+
+
+### Add Environment Variables
+
+Before you can start using the Contentful Module, you need to add the necessary environment variables used in the module's options.
+
+Add the following environment variables to your `.env` file:
+
+```plain
+CONTENTFUL_MANAGEMNT_ACCESS_TOKEN=CFPAT-...
+CONTENTFUL_DELIVERY_TOKEN=eij...
+CONTENTFUL_SPACE_ID=t2a...
+CONTENTFUL_ENVIRONMENT=master
+```
+
+Where:
+
+- `CONTENTFUL_MANAGEMNT_ACCESS_TOKEN`: The Contentful management API access token. To create it on the Contentful dashboard:
+ - Click on the cog icon at the top right, then choose "CMA tokens" from the dropdown.
+
+
+
+- In the CMA tokens page, click on the "Create personal access token" button.
+- In the window that pops up, enter a name for the token, and choose an expiry date. Once you're done, click the Generate button.
+- The token is generated and shown in the pop-up. Make sure to copy it and use it in the `.env` file, as you can't access it again.
+
+
+
+- `CONTENTFUL_DELIVERY_TOKEN`: An API token that you can use with the delivery API. To create it on the Contentful dashboard:
+ - Click on the cog icon at the top right, then choose "API keys" from the dropdown.
+
+
+
+- In the APIs page, click on the "Add API key" button.
+- In the window that pops up, enter a name for the token, then click the Add API Key button.
+- This will create an API key and opens its page. On its page, copy the token for the "Content Delivery API" and use it as the value for `CONTENTFUL_DELIVERY_TOKEN`.
+
+
+
+- `CONTENTFUL_SPACE_ID`: The ID of your Contentful space. You can copy this from the dashboard's URL which is of the format `https://app.contentful.com/spaces/{space_id}/...`.
+- `CONTENTFUL_ENVIRONMENT`: The environment to manage and retrieve the content in. By default, you have the `master` environment which you can use. However, you can use another Contentful environment that you've created.
+
+Your module is now ready for use.
+
+### Test the Module
+
+To test out the module, you'll start the Medusa application, which will run the module's loader.
+
+To start the Medusa application, run the following command:
+
+```bash npm2yarn
+npm run dev
+```
+
+If the loader ran successfully, you'll see the following message in the terminal:
+
+```bash
+info: Connected to Contentful
+```
+
+You can also see on the Contentful dashboard that the content types were created. To view them, go to the Content Model page.
+
+
+
+***
+
+## Step 3: Create Products in Contentful
+
+Now that you have the Contentful Module ready for use, you can start creating products in Contentful.
+
+In this step, you'll implement the logic to create products in Contentful. Later, you'll execute it when:
+
+- A product is created in Medusa.
+- The admin user triggers a sync manually.
+
+### Add Methods to Contentful Module Service
+
+To create products in Contentful, you need to add the necessary methods in the Contentful Module's service. Then, you can use these methods later when building the creation flow.
+
+To create a product in Contentful, you'll need three methods: One to create the product's variants, another to create the product's options and values, and a third to create the product.
+
+In the service at `src/modules/contentful/service.ts`, start by adding the method to create the product's variants:
+
+```ts title="src/modules/contentful/service.ts"
+// imports...
+import { ProductVariantDTO } from "@medusajs/framework/types"
+import { EntryProps } from "contentful-management"
+
+export default class ContentfulModuleService {
+ // ...
+
+ private async createProductVariant(
+ variants: ProductVariantDTO[],
+ productEntry: EntryProps
+ ) {
+ for (const variant of variants) {
+ await this.managementClient.entry.createWithId(
+ {
+ contentTypeId: "productVariant",
+ entryId: variant.id,
+ },
+ {
+ fields: {
+ medusaId: {
+ [this.options.default_locale!]: variant.id,
+ },
+ title: {
+ [this.options.default_locale!]: variant.title,
+ },
+ product: {
+ [this.options.default_locale!]: {
+ sys: {
+ type: "Link",
+ linkType: "Entry",
+ id: productEntry.sys.id,
+ },
+ },
+ },
+ productOptionValues: {
+ [this.options.default_locale!]: variant.options.map((option) => ({
+ sys: {
+ type: "Link",
+ linkType: "Entry",
+ id: option.id,
+ },
+ })),
+ },
+ },
+ }
+ )
+ }
+ }
+}
+```
+
+You define a private method `createProductVariant` that accepts two parameters:
+
+1. The product's variants to create in Contentful.
+2. The product's entry in Contentful.
+
+In the method, you iterate over the product's variants and create a new entry in Contentful for each variant. You set the fields based on the product variant content type you created earlier.
+
+For each field, you specify the value for the default locale. In the Contentful dashboard, you can manage the values for other locales.
+
+Next, add the method to create the product's options and values:
+
+```ts title="src/modules/contentful/service.ts" highlights={createProductOptionHighlights}
+// other imports...
+import { ProductOptionDTO } from "@medusajs/framework/types"
+
+export default class ContentfulModuleService {
+ // ...
+ private async createProductOption(
+ options: ProductOptionDTO[],
+ productEntry: EntryProps
+ ) {
+ for (const option of options) {
+ const valueIds: {
+ sys: {
+ type: "Link",
+ linkType: "Entry",
+ id: string
+ }
+ }[] = []
+ for (const value of option.values) {
+ await this.managementClient.entry.createWithId(
+ {
+ contentTypeId: "productOptionValue",
+ entryId: value.id,
+ },
+ {
+ fields: {
+ value: {
+ [this.options.default_locale!]: value.value,
+ },
+ medusaId: {
+ [this.options.default_locale!]: value.id,
+ },
+ },
+ }
+ )
+ valueIds.push({
+ sys: {
+ type: "Link",
+ linkType: "Entry",
+ id: value.id,
+ },
+ })
+ }
+ await this.managementClient.entry.createWithId(
+ {
+ contentTypeId: "productOption",
+ entryId: option.id,
+ },
+ {
+ fields: {
+ medusaId: {
+ [this.options.default_locale!]: option.id,
+ },
+ title: {
+ [this.options.default_locale!]: option.title,
+ },
+ product: {
+ [this.options.default_locale!]: {
+ sys: {
+ type: "Link",
+ linkType: "Entry",
+ id: productEntry.sys.id,
+ },
+ },
+ },
+ values: {
+ [this.options.default_locale!]: valueIds,
+ },
+ },
+ }
+ )
+ }
+ }
+}
+```
+
+You define a private method `createProductOption` that accepts two parameters:
+
+1. The product's options, which is an array of objects.
+2. The product's entry in Contentful, which is an object.
+
+In the method, you iterate over the product's options and create entries for each of its values. Then, you create an entry for the option, and reference the values you created in Contentful. You set the fields based on the option and value content types you created earlier.
+
+Finally, add the method to create the product:
+
+```ts title="src/modules/contentful/service.ts" highlights={createProductHighlights}
+// other imports...
+import { ProductDTO } from "@medusajs/framework/types"
+
+export default class ContentfulModuleService {
+ // ...
+ async createProduct(
+ product: ProductDTO
+ ) {
+ try {
+ // check if product already exists
+ const productEntry = await this.managementClient.entry.get({
+ environmentId: this.options.environment,
+ entryId: product.id,
+ })
+
+ return productEntry
+ } catch(e) {}
+
+ // Create product entry in Contentful
+ const productEntry = await this.managementClient.entry.createWithId(
+ {
+ contentTypeId: "product",
+ entryId: product.id,
+ },
+ {
+ fields: {
+ medusaId: {
+ [this.options.default_locale!]: product.id,
+ },
+ title: {
+ [this.options.default_locale!]: product.title,
+ },
+ description: product.description ? {
+ [this.options.default_locale!]: {
+ nodeType: "document",
+ data: {},
+ content: [
+ {
+ nodeType: "paragraph",
+ data: {},
+ content: [
+ {
+ nodeType: "text",
+ value: product.description,
+ marks: [],
+ data: {},
+ },
+ ],
+ },
+ ],
+ },
+ } : undefined,
+ subtitle: product.subtitle ? {
+ [this.options.default_locale!]: product.subtitle,
+ } : undefined,
+ handle: product.handle ? {
+ [this.options.default_locale!]: product.handle,
+ } : undefined,
+ },
+ }
+ )
+
+ // Create options if they exist
+ if (product.options?.length) {
+ await this.createProductOption(product.options, productEntry)
+ }
+
+ // Create variants if they exist
+ if (product.variants?.length) {
+ await this.createProductVariant(product.variants, productEntry)
+ }
+
+ // update product entry with variants and options
+ await this.managementClient.entry.update(
+ {
+ entryId: productEntry.sys.id,
+ },
+ {
+ sys: productEntry.sys,
+ fields: {
+ ...productEntry.fields,
+ productVariants: {
+ [this.options.default_locale!]: product.variants?.map((variant) => ({
+ sys: {
+ type: "Link",
+ linkType: "Entry",
+ id: variant.id,
+ },
+ })),
+ },
+ productOptions: {
+ [this.options.default_locale!]: product.options?.map((option) => ({
+ sys: {
+ type: "Link",
+ linkType: "Entry",
+ id: option.id,
+ },
+ })),
+ },
+ },
+ }
+ )
+
+ return productEntry
+ }
+}
+```
+
+You define a public method `createProduct` that accepts a product object as a parameter.
+
+In the method, you first check if the product already exists in Contentful. If it does, you return the existing product entry. Otherwise, you create a new product entry with the fields based on the product content type you created earlier.
+
+Next, you create entries for the product's options and variants using the methods you created earlier.
+
+Finally, you update the product entry to reference the variants and options you created.
+
+You now have all the methods to create products in Contentful. You'll also need one last method to delete a product in Contentful. This is useful when you implement the rollback mechanism in the flow that creates the products.
+
+Add the following method to the service:
+
+```ts title="src/modules/contentful/service.ts" highlights={deleteProductHighlights}
+// other imports...
+import { MedusaError } from "@medusajs/framework/utils"
+
+export default class ContentfulModuleService {
+ // ...
+ async deleteProduct(productId: string) {
+ try {
+ // Get the product entry
+ const productEntry = await this.managementClient.entry.get({
+ environmentId: this.options.environment,
+ entryId: productId,
+ })
+
+ if (!productEntry) {
+ return
+ }
+
+ // Delete the product entry
+ await this.managementClient.entry.unpublish({
+ environmentId: this.options.environment,
+ entryId: productId,
+ })
+
+ await this.managementClient.entry.delete({
+ environmentId: this.options.environment,
+ entryId: productId,
+ })
+
+ // Delete the product variant entries
+ for (const variant of productEntry.fields.productVariants[this.options.default_locale!]) {
+ await this.managementClient.entry.unpublish({
+ environmentId: this.options.environment,
+ entryId: variant.sys.id,
+ })
+
+ await this.managementClient.entry.delete({
+ environmentId: this.options.environment,
+ entryId: variant.sys.id,
+ })
+ }
+
+ // Delete the product options entries and values
+ for (const option of productEntry.fields.productOptions[this.options.default_locale!]) {
+ for (const value of option.fields.values[this.options.default_locale!]) {
+ await this.managementClient.entry.unpublish({
+ environmentId: this.options.environment,
+ entryId: value.sys.id,
+ })
+
+ await this.managementClient.entry.delete({
+ environmentId: this.options.environment,
+ entryId: value.sys.id,
+ })
+ }
+
+ await this.managementClient.entry.unpublish({
+ environmentId: this.options.environment,
+ entryId: option.sys.id,
+ })
+
+ await this.managementClient.entry.delete({
+ environmentId: this.options.environment,
+ entryId: option.sys.id,
+ })
+ }
+ } catch (error) {
+ throw new MedusaError(
+ MedusaError.Types.INVALID_DATA,
+ `Failed to delete product from Contentful: ${error.message}`
+ )
+ }
+ }
+}
+```
+
+You define a public method `deleteProduct` that accepts a product ID as a parameter.
+
+In the method, you retrieve the product entry from Contentful with its variants, options, and values. For each entry, you must unpublish and delete it.
+
+You now have all the methods necessary to build the creation flow.
+
+### Create Contentful Product Workflow
+
+To implement the logic that's triggered when a product is created in Medusa, or when the admin user triggers a sync manually, you need to create a workflow.
+
+A [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) is a series of actions, called steps, that complete a task. You construct a workflow like you construct a function, but it's a special function that allows you to track its executions' progress, define roll-back logic, and configure other advanced features.
+
+Learn more about workflows in the [Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).
+
+In this section, you'll create a workflow that creates Medusa products in Contentful using the Contentful Module.
+
+The workflow has the following steps:
+
+- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve products to create in Contentful.
+- [createProductsContentfulStep](#createProductsContentfulStep): Create the products in Contentful.
+
+Medusa provides the `useQueryGraphStep` in its `@medusajs/medusa/core-flows` package. So, you only need to implement the second step.
+
+#### createProductsContentfulStep
+
+In the second step, you create the retrieved products in Contentful.
+
+To create the step, create the file `src/workflows/steps/create-products-contentful.ts` with the following content:
+
+```ts title="src/workflows/steps/create-products-contentful.ts" highlights={createProductsContentfulStepHighlights}
+import { ProductDTO } from "@medusajs/framework/types"
+import { CONTENTFUL_MODULE } from "../../modules/contentful"
+import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
+import ContentfulModuleService from "../../modules/contentful/service"
+import { EntryProps } from "contentful-management"
+
+type StepInput = {
+ products: ProductDTO[]
+}
+
+export const createProductsContentfulStep = createStep(
+ "create-products-contentful-step",
+ async (input: StepInput, { container }) => {
+ const contentfulModuleService: ContentfulModuleService =
+ container.resolve(CONTENTFUL_MODULE)
+
+ const products: EntryProps[] = []
+
+ try {
+ for (const product of input.products) {
+ products.push(await contentfulModuleService.createProduct(product))
+ }
+ } catch(e) {
+ return StepResponse.permanentFailure(
+ `Error creating products in Contentful: ${e.message}`,
+ products
+ )
+ }
+
+ return new StepResponse(
+ products,
+ products
+ )
+ },
+ async (products, { container }) => {
+ if (!products) {
+ return
+ }
+
+ const contentfulModuleService: ContentfulModuleService =
+ container.resolve(CONTENTFUL_MODULE)
+
+ for (const product of products) {
+ await contentfulModuleService.deleteProduct(product.sys.id)
+ }
+ }
+)
+```
+
+You create a step with `createStep` from the Workflows SDK. It accepts three parameters:
+
+1. The step's unique name, which is `create-products-contentful-step`.
+2. An async function that receives two parameters:
+ - The step's input, which is in this case an object holding an array of products to create in Contentful.
+ - 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.
+3. An optional compensation function that undoes the actions performed in the step if an error occurs in the workflow's execution. This mechanism ensures data consistency in your application, especially as you integrate external systems.
+
+The Medusa container is different from the module's container. Since modules are isolated, they each have a container with their resources. Refer to the [Module Container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md) documentation for more information.
+
+In the step function, you resolve the Contentful Module's service from the Medusa container using the name you exported in the module definition's file.
+
+Then, you iterate over the products and create a new entry in Contentful for each product using the `createProduct` method you created earlier. If the creation of any product fails, you fail the step and pass the created products to the compensation function.
+
+A step function must return a `StepResponse` instance. The `StepResponse` constructor accepts two parameters:
+
+1. The step's output, which is the product entries created in Contentful.
+2. Data to pass to the step's compensation function.
+
+The compensation function accepts as a parameter the data passed from the step, and an object containing the Medusa container.
+
+In the compensation function, you iterate over the created product entries and delete them from Contentful using the `deleteProduct` method you created earlier.
+
+#### Create the Workflow
+
+Now that you have all the necessary steps, you can create the workflow.
+
+To create the workflow, create the file `src/workflows/create-products-contentful.ts` with the following content:
+
+```ts title="src/workflows/create-products-contentful.ts"
+import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
+import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
+import { createProductsContentfulStep } from "./steps/create-products-contentful"
+import { ProductDTO } from "@medusajs/framework/types"
+
+type WorkflowInput = {
+ product_ids: string[]
+}
+
+export const createProductsContentfulWorkflow = createWorkflow(
+ { name: "create-products-contentful-workflow" },
+ (input: WorkflowInput) => {
+ // @ts-ignore
+ const { data } = useQueryGraphStep({
+ entity: "product",
+ fields: [
+ "id",
+ "title",
+ "description",
+ "subtitle",
+ "status",
+ "handle",
+ "variants.*",
+ "variants.options.*",
+ "options.*",
+ "options.values.*",
+ ],
+ filters: {
+ id: input.product_ids,
+ },
+ })
+
+ const contentfulProducts = createProductsContentfulStep({
+ products: data as ProductDTO[],
+ })
+
+ return new WorkflowResponse(contentfulProducts)
+ }
+)
+```
+
+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 the product IDs to create in Contentful.
+
+In the workflow's constructor function, you:
+
+1. Retrieve the Medusa products using the `useQueryGraphStep` helper step. This step uses Medusa's [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) tool to retrieve data across modules. You pass it the product IDs to retrieve.
+2. Create the product entries in Contentful using the `createProductsContentfulStep` step.
+
+A workflow must return an instance of `WorkflowResponse`. The `WorkflowResponse` constructor accepts the workflow's output as a parameter, which is an object of the product entries created in Contentful.
+
+You now have the workflow that you can execute when a product is created in Medusa, or when the admin user triggers a sync manually.
+
+***
+
+## Step 4: Trigger Sync on Product Creation
+
+Medusa has an event system that allows you to listen for events, such as `product.created`, and perform an asynchronous action when the event is emitted.
+
+You listen to events in a subscriber. A [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) is an asynchronous function that listens to one or more events and performs actions when these events are emitted. A subscriber is useful when syncing data across systems, as the operation can be time-consuming and should be performed in the background.
+
+In this step, you'll create a subscriber that listens to the `product.created` event and executes the `createProductsContentfulWorkflow` workflow.
+
+Learn more about subscribers in the [Events and Subscribers documentation](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md).
+
+To create a subscriber, create the file `src/subscribers/create-product.ts` with the following content:
+
+```ts title="src/subscribers/create-product.ts" highlights={createProductSubscriberHighlights}
+import {
+ type SubscriberConfig,
+ type SubscriberArgs,
+} from "@medusajs/framework"
+import {
+ createProductsContentfulWorkflow,
+} from "../workflows/create-products-contentful"
+
+export default async function handleProductCreate({
+ event: { data },
+ container,
+}: SubscriberArgs<{ id: string }>) {
+ await createProductsContentfulWorkflow(container)
+ .run({
+ input: {
+ product_ids: [data.id],
+ },
+ })
+
+ console.log("Product created in Contentful")
+}
+
+export const config: SubscriberConfig = {
+ event: "product.created",
+}
+```
+
+A subscriber file must export:
+
+1. An asynchronous function, which is the subscriber that is executed when the event is emitted.
+2. A configuration object that holds the name of the event the subscriber listens to, which is `product.created` in this case.
+
+The subscriber function receives an object as a parameter that has the following properties:
+
+- `event`: An object that holds the event's data payload. The payload of the `product.created` event is an array of product IDs.
+- `container`: The Medusa container to access the Framework and commerce tools.
+
+In the subscriber function, you execute the `createProductsContentfulWorkflow` by invoking it, passing the Medusa container as a parameter. Then, you chain a `run` method, passing it the product ID from the event's data payload as input.
+
+Finally, you log a message to the console to indicate that the product was created in Contentful.
+
+### Test the Subscriber
+
+To test out the subscriber, start the Medusa application:
+
+```bash npm2yarn
+npm run dev
+```
+
+Then, open the Medusa Admin dashboard and login.
+
+Can't remember the credentials? Learn how to create a user in the [Medusa CLI reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/medusa-cli/commands/user/index.html.md).
+
+Next, open the Products page and create a new product.
+
+You should see the following message in the terminal:
+
+```bash
+info: Product created in Contentful
+```
+
+You can also see the product in the Contentful dashboard by going to the Content page.
+
+***
+
+## Step 5: Trigger Product Sync Manually
+
+The other way to sync products is when the admin user triggers a sync manually. This is useful when you already have products in Medusa and you want to sync them to Contentful.
+
+To allow admin users to trigger a sync manually, you need:
+
+1. A subscriber that listens to a custom event.
+2. An API route that emits the custom event when a request is sent to it.
+3. A UI route in the Medusa Admin that displays a button to trigger the sync.
+
+### Create Manual Sync Subscriber
+
+You'll start by creating the subscriber that listens to a custom event to sync the Medusa products to Contentful.
+
+To create the subscriber, create the file `src/subscribers/sync-products.ts` with the following content:
+
+```ts title="src/subscribers/sync-products.ts" highlights={syncProductsSubscriberHighlights}
+import type {
+ SubscriberConfig,
+ SubscriberArgs,
+} from "@medusajs/framework"
+import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
+import {
+ createProductsContentfulWorkflow,
+} from "../workflows/create-products-contentful"
+
+export default async function syncProductsHandler({
+ container,
+}: SubscriberArgs>) {
+ const query = container.resolve(ContainerRegistrationKeys.QUERY)
+
+ const batchSize = 100
+ let hasMore = true
+ let offset = 0
+ let totalCount = 0
+
+ while (hasMore) {
+ const {
+ data: products,
+ metadata: { count } = {},
+ } = await query.graph({
+ entity: "product",
+ fields: [
+ "id",
+ ],
+ pagination: {
+ skip: offset,
+ take: batchSize,
+ },
+ })
+
+ if (products.length) {
+ await createProductsContentfulWorkflow(container).run({
+ input: {
+ product_ids: products.map((product) => product.id),
+ },
+ })
+ }
+
+ hasMore = products.length === batchSize
+ offset += batchSize
+ totalCount = count ?? 0
+ }
+
+ console.log(`Synced ${totalCount} products to Contentful`)
+}
+
+export const config: SubscriberConfig = {
+ event: "products.sync",
+}
+```
+
+You create a subscriber that listens to the `products.sync` event.
+
+In the subscriber function, you use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to retrieve all the products in Medusa with pagination. Then, for each batch of products, you execute the `createProductsContentfulWorkflow` workflow, passing the product IDs to the workflow.
+
+Finally, you log a message to the console to indicate that the products were synced to Contentful.
+
+### Create API Route to Trigger Sync
+
+Next, to allow the admin user to trigger the sync manually, you need to create an API route that emits the `products.sync` event.
+
+An API Route is an endpoint that exposes commerce features to external applications and clients, such as storefronts.
+
+Learn more about API routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md).
+
+An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`.
+
+So, to create an API route at the path `/admin/contentful/sync`, create the file `src/api/admin/contentful/sync/route.ts` with the following content:
+
+```ts title="src/api/admin/contentful/sync/route.ts" highlights={syncProductsRouteHighlights}
+import {
+ MedusaRequest,
+ MedusaResponse,
+} from "@medusajs/framework/http"
+
+export const POST = async (
+ req: MedusaRequest,
+ res: MedusaResponse
+) => {
+ const eventService = req.scope.resolve("event_bus")
+
+ await eventService.emit({
+ name: "products.sync",
+ data: {},
+ })
+
+ res.status(200).json({
+ message: "Products sync triggered successfully",
+ })
+}
+```
+
+Since you export a `POST` route handler function, you expose an `API` route at `/admin/contentful/sync`. The route handler function accepts two parameters:
+
+1. A request object with details and context on the request, such as body parameters or authenticated user details.
+2. A response object to manipulate and send the response.
+
+In the route handler, you resolve the [Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/index.html.md)'s service from the Medusa container and emit the `products.sync` event.
+
+### Create UI Route to Trigger Sync
+
+Finally, you'll add a new page to the Medusa Admin dashboard that displays a button to trigger the sync. To add a page, you need to create a UI route.
+
+A [UI route](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md) is a React component that specifies the content to be shown in a new page of the Medusa Admin dashboard. You'll create a UI route to display a button that triggers product syncing to Contentful when clicked.
+
+Refer to the [UI Routes](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md) documentation for more information.
+
+#### Configure JS SDK
+
+Before creating the UI route, you'll configure Medusa's [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md) so that you can use it to send requests to the Medusa server.
+
+The JS SDK is installed by default in your Medusa application. To configure it, create the file `src/admin/lib/sdk.ts` with the following content:
+
+```ts title="src/admin/lib/sdk.ts"
+import Medusa from "@medusajs/js-sdk"
+
+export const sdk = new Medusa({
+ baseUrl: "http://localhost:9000",
+ debug: process.env.NODE_ENV === "development",
+ auth: {
+ type: "session",
+ },
+})
+```
+
+You create an instance of the JS SDK using the `Medusa` class from the JS SDK. You pass it an object having the following properties:
+
+- `baseUrl`: The base URL of the Medusa server.
+- `debug`: A boolean indicating whether to log debug information into the console.
+- `auth`: An object specifying the authentication type. When using the JS SDK for admin customizations, you use the `session` authentication type.
+
+#### Create UI Route
+
+UI routes are created in a `page.tsx` file under a sub-directory of `src/admin/routes` directory. The file's path relative to `src/admin/routes` determines its path in the dashboard.
+
+So, create the file `src/admin/routes/contentful/page.tsx` with the following content:
+
+```tsx title="src/admin/routes/contentful/page.tsx" highlights={contentfulPageHighlights}
+import { defineRouteConfig } from "@medusajs/admin-sdk"
+import { Container, Heading, Button } from "@medusajs/ui"
+import { useMutation } from "@tanstack/react-query"
+import { sdk } from "../../lib/sdk"
+import { toast } from "@medusajs/ui"
+
+const ContentfulSettingsPage = () => {
+ const { mutate, isPending } = useMutation({
+ mutationFn: () =>
+ sdk.client.fetch("/admin/contentful/sync", {
+ method: "POST",
+ }),
+ onSuccess: () => {
+ toast.success("Sync to Contentful triggered successfully")
+ },
+ })
+
+ return (
+
+
+
+ Contentful Settings
+
+
+
+
+
+
+ )
+}
+
+export const config = defineRouteConfig({
+ label: "Contentful",
+})
+
+export default ContentfulSettingsPage
+```
+
+A UI route's file must export:
+
+1. A React component that defines the content of the page.
+2. A configuration object that specifies the route's label in the dashboard. This label is used to show a sidebar item for the new route.
+
+In the React component, you use `useMutation` hook from `@tanstack/react-query` to create a mutation that sends a `POST` request to the API route you created earlier. In the mutation function, you use the JS SDK to send the request.
+
+Then, in the return statement, you display a button that triggers the mutation when clicked, which sends a request to the API route you created earlier.
+
+### Test the Sync
+
+To test out the sync, start the Medusa application:
+
+```bash npm2yarn
+npm run dev
+```
+
+Then, open the Medusa Admin dashboard and login. In the sidebar, you'll find a new "Contentful" item. If you click on it, you'll see the page you created with the button to trigger the sync.
+
+
+
+If you click on the button, you'll see the following message in the terminal:
+
+```bash
+info: Synced 4 products to Contentful
+```
+
+Assuming you have `4` products in Medusa, the message indicates that the sync was successful.
+
+You can also see the products in the Contentful dashboard.
+
+
+
+***
+
+## Step 6: Retrieve Locales API Route
+
+In the next steps, you'll implement customizations that are useful for storefronts. A storefront should show the customer a list of available locales and allow them to select from them.
+
+In this step, you will:
+
+1. Add the logic to retrieve locales from Contentful in the Contentful Module's service.
+2. Create an API route that exposes the locales to the storefront.
+3. Customize the Next.js Starter Storefront to show the locales to customers.
+
+### Retrieve Locales from Contentful Method
+
+You'll start by adding two methods to the Contentful Module's service that are useful to retrieve locales from Contentful.
+
+The first method retrieves all locales from Contentful. Add it to the service at `src/modules/contentful/service.ts`:
+
+```ts title="src/modules/contentful/service.ts"
+export default class ContentfulModuleService {
+ // ...
+ async getLocales() {
+ return await this.managementClient.locale.getMany({})
+ }
+}
+```
+
+You use the `locale.getMany` method of the Contentful Management API client to retrieve all locales.
+
+The second method returns the code of the default locale:
+
+```ts title="src/modules/contentful/service.ts"
+export default class ContentfulModuleService {
+ // ...
+ async getDefaultLocaleCode() {
+ return this.options.default_locale
+ }
+}
+```
+
+You return the default locale using the `default_locale` option you set in the module's options.
+
+### Create API Route to Retrieve Locales
+
+Next, you'll create an API route that exposes the locales to the storefront.
+
+To create the API route, create the file `src/api/store/locales/route.ts` with the following content:
+
+```ts title="src/api/store/locales/route.ts" highlights={getLocalesRouteHighlights}
+import {
+ MedusaRequest,
+ MedusaResponse,
+} from "@medusajs/framework/http"
+import { CONTENTFUL_MODULE } from "../../../modules/contentful"
+import ContentfulModuleService from "../../../modules/contentful/service"
+
+export const GET = async (
+ req: MedusaRequest,
+ res: MedusaResponse
+) => {
+ const contentfulModuleService: ContentfulModuleService = req.scope.resolve(
+ CONTENTFUL_MODULE
+ )
+
+ const locales = await contentfulModuleService.getLocales()
+ const defaultLocaleCode = await contentfulModuleService.getDefaultLocaleCode()
+
+ const formattedLocales = locales.items.map((locale) => {
+ return {
+ name: locale.name,
+ code: locale.code,
+ is_default: locale.code === defaultLocaleCode,
+ }
+ })
+
+ res.json({
+ locales: formattedLocales,
+ })
+}
+```
+
+Since you export a `GET` route handler function, you expose a `GET` route at `/store/locales`.
+
+In the route handler, you resolve the Contentful Module's service from the Medusa container to retrieve the locales and the default locale code.
+
+Then, you format the locales to include their name, code, and whether they are the default locale.
+
+Finally, you return the formatted locales in the JSON response.
+
+### Customize Storefront to Show Locales
+
+In the first step of this tutorial, you installed the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) along with the Medusa application. This storefront provides ecommerce features like a product catalog, a cart, and a checkout.
+
+In this section, you'll customize the storefront to show the locales to customers and allow them to select from them. The selected locale will be stored in the browser's cookies, allowing you to use it later when retrieving a product's localized data.
+
+The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.
+
+So, if your Medusa application's directory is `medusa-contentful`, you can find the storefront by going back to the parent directory and changing to the `medusa-contentful-storefront` directory:
+
+```bash
+cd ../medusa-contentful-storefront # change based on your project name
+```
+
+#### Add Cookie Functions
+
+You'll start by adding two functions that retrieve and set the locale in the browser's cookies.
+
+In `src/lib/data/cookies.ts` add the following functions:
+
+```ts title="src/lib/data/cookies.ts" highlights={getLocaleHighlights} badgeLabel="Storefront" badgeColor="blue"
+export const getLocale = async () => {
+ const cookies = await nextCookies()
+ return cookies.get("_medusa_locale")?.value
+}
+
+export const setLocale = async (locale: string) => {
+ const cookies = await nextCookies()
+ cookies.set("_medusa_locale", locale, {
+ maxAge: 60 * 60 * 24 * 7,
+ })
+}
+```
+
+The `getLocale` function retrieves the locale from the browser's cookies, and the `setLocale` function sets the locale in the browser's cookies.
+
+#### Manage Locales Functions
+
+Next, you'll add server actions to retrieve the locales and set the selected locale.
+
+Create the file `src/lib/data/locale.ts` with the following content:
+
+```ts title="src/lib/data/locale.ts" highlights={getLocalesHighlights} badgeLabel="Storefront" badgeColor="blue"
+"use server"
+
+import { sdk } from "@lib/config"
+import type { Document } from "@contentful/rich-text-types"
+import { getLocale, setLocale } from "./cookies"
+
+export type Locale = {
+ name: string
+ code: string
+ is_default: boolean
+}
+
+export async function getLocales() {
+ return await sdk.client.fetch<{
+ locales: Locale[]
+ }>("/store/locales")
+}
+
+export async function getSelectedLocale() {
+ let localeCode = await getLocale()
+ if (!localeCode) {
+ const locales = await getLocales()
+ localeCode = locales.locales.find((l) => l.is_default)?.code
+ }
+ return localeCode
+}
+
+export async function setSelectedLocale(locale: string) {
+ await setLocale(locale)
+}
+```
+
+You add the following functions:
+
+1. `getLocales`: Retrieves the locales from the Medusa server using the API route you created earlier.
+2. `getSelectedLocale`: Retrieves the selected locale from the browser's cookies, or the default locale if no locale is selected.
+3. `setSelectedLocale`: Sets the selected locale in the browser's cookies.
+
+You'll use these functions as you add the UI to show the locales next.
+
+#### Show Locales in the Storefront
+
+You'll now add the UI to show the locales to customers and allow them to select from them.
+
+Create the file `src/modules/layout/components/locale-select/index.tsx` with the following content:
+
+```tsx title="src/modules/layout/components/locale-select/index.tsx" highlights={localeSelectHighlights} badgeLabel="Storefront" badgeColor="blue"
+"use client"
+
+import { useState, useEffect, Fragment } from "react"
+import { getLocales, Locale, getSelectedLocale, setSelectedLocale } from "../../../../lib/data/locale"
+import { Listbox, ListboxButton, ListboxOption, ListboxOptions, Transition } from "@headlessui/react"
+import { ArrowRightMini } from "@medusajs/icons"
+import { clx } from "@medusajs/ui"
+
+const LocaleSelect = () => {
+ const [locales, setLocales] = useState([])
+ const [locale, setLocale] = useState()
+ const [open, setOpen] = useState(false)
+
+ useEffect(() => {
+ getLocales()
+ .then(({ locales }) => {
+ setLocales(locales)
+ })
+ }, [])
+
+ useEffect(() => {
+ if (!locales.length || locale) {
+ return
+ }
+
+ getSelectedLocale().then((locale) => {
+ const localeDetails = locales.find((l) => l.code === locale)
+ setLocale(localeDetails)
+ })
+ }, [locales])
+
+ useEffect(() => {
+ if (locale) {
+ setSelectedLocale(locale.code)
+ }
+ }, [locale])
+
+ const handleChange = (locale: Locale) => {
+ setLocale(locale)
+ setOpen(false)
+ }
+
+ // TODO add return statement
+}
+
+export default LocaleSelect
+```
+
+You create a `LocaleSelect` component with the following state variables:
+
+1. `locales`: The list of locales retrieved from the Medusa server.
+2. `locale`: The selected locale.
+3. `open`: A boolean indicating whether the dropdown is open.
+
+Then, you use three `useEffect` hooks:
+
+1. The first `useEffect` hook retrieves the locales using the `getLocales` function and sets them in the `locales` state variable.
+2. The second `useEffect` hook is triggered when the `locales` state variable changes. It retrieves the selected locale using the `getSelectedLocale` function and sets the `locale` state variable.
+3. The third `useEffect` hook is triggered when the `locale` state variable changes. It sets the selected locale in the browser's cookies using the `setSelectedLocale` function.
+
+You also create a `handleChange` function that sets the selected locale and closes the dropdown. You'll execute this function when the customer selects a locale from the dropdown.
+
+Finally, you'll add a return statement that shows the locale dropdown. Replace the `TODO` with the following:
+
+```tsx title="src/modules/layout/components/locale-select/index.tsx" badgeLabel="Storefront" badgeColor="blue"
+return (
+
+)
+```
+
+You show the selected locale. Then, when the customer hovers over the locale, the dropdown is shown to select a different locale.
+
+When the customer selects a locale, you execute the `handleChange` function, which sets the selected locale and closes the dropdown.
+
+#### Add Locale Select to the Side Menu
+
+The last step is to show the locale selector in the side menu after the country selector.
+
+In `src/modules/layout/components/side-menu/index.tsx`, add the following import:
+
+```tsx title="src/modules/layout/components/side-menu/index.tsx" badgeLabel="Storefront" badgeColor="blue"
+import LocaleSelect from "../locale-select"
+```
+
+Then, add the `LocaleSelect` component in the return statement of the `SideMenu` component, after the `div` wrapping the country selector:
+
+```tsx title="src/modules/layout/components/side-menu/index.tsx" badgeLabel="Storefront" badgeColor="blue"
+
+```
+
+The locale selector will now show in the side menu after the country selector.
+
+### Test out the Locale Selector
+
+To test out all the changes made in this step, start the Medusa application by running the following command in the Medusa application's directory:
+
+```bash npm2yarn
+npm run dev
+```
+
+Then, start the Next.js Starter Storefront by running the following command in the storefront's directory:
+
+```bash npm2yarn
+npm run dev
+```
+
+The storefront will run at `http://localhost:8000`. Open it in your browser, then click on "Menu" at the top right. You'll see at the bottom of the side menu the locale selector.
+
+
+
+You can try selecting a different locale. The selected locale will be stored, but products will still be shown in the default locale. You'll implement the locale-based product retrieval in the next step.
+
+***
+
+## Step 7: Retrieve Product Details for Locale
+
+The next feature you'll implement is retrieving and displaying product details for a selected locale.
+
+You'll implement this feature by:
+
+1. Linking Medusa's product to Contentful's product.
+2. Adding the method to retrieve product details for a selected locale from Contentful.
+3. Adding a new route to retrieve the product details for a selected locale.
+4. Customizing the storefront to show the product details for the selected locale.
+
+### Link Medusa's Product to Contentful's Product
+
+Medusa facilitates retrieving data across systems using [module links](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index.html.md). A module link forms an association between data models of two modules while maintaining module isolation.
+
+Not only do module links support Medusa data models, but they also support virtual data models that are not persisted in Medusa's database. In that case, you create a [read-only module link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/read-only/index.html.md) that allows you to retrieve data across systems.
+
+In this section, you'll define a read-only module link between Medusa's product and Contentful's product, allowing you to later retrieve a product's entry in Contentful within a single query.
+
+Learn more about read-only module links in the [Read-Only Module Links](https://docs.medusajs.com/docs/learn/fundamentals/module-links/read-only/index.html.md) documentation.
+
+Module links are defined in a TypeScript or JavaScript file under the `src/links` directory. So, create the file `src/links/product-contentful.ts` with the following content:
+
+```ts title="src/links/product-contentful.ts" highlights={productContentfulLinkHighlights}
+import { defineLink } from "@medusajs/framework/utils"
+import ProductModule from "@medusajs/medusa/product"
+import { CONTENTFUL_MODULE } from "../modules/contentful"
+
+export default defineLink(
+ {
+ linkable: ProductModule.linkable.product,
+ field: "id",
+ },
+ {
+ linkable: {
+ serviceName: CONTENTFUL_MODULE,
+ alias: "contentful_product",
+ primaryKey: "product_id",
+ },
+ },
+ {
+ readOnly: true,
+ }
+)
+```
+
+You define a module link using `defineLink` from the Modules SDK. It accepts three parameters:
+
+1. An object with the linkable configuration of the data model in Medusa, and the field that will be passed as a filter to the Contentful Module's service.
+2. An object with the linkable configuration of the virtual data model in Contentful. This object must have the following properties:
+ - `serviceName`: The name of the service, which is the Contentful Module's name. Medusa uses this name to resolve the module's service from the Medusa container.
+ - `alias`: The alias to use when querying the linked records. You'll see how that works in a bit.
+ - `primaryKey`: The field in Contentful's virtual data model that holds the ID of a product.
+3. An object with the `readOnly` property set to `true`.
+
+You'll see how the module link works in the upcoming steps.
+
+### List Contentful Products Method
+
+Next, you'll add a method that lists Contentful products for a given locale.
+
+Add the following method to the Contentful Module's service at `src/modules/contentful/service.ts`:
+
+```ts title="src/modules/contentful/service.ts" highlights={listContentfulProductsMethodHighlights}
+export default class ContentfulModuleService {
+ // ...
+ async list(
+ filter: {
+ id: string | string[]
+ context?: {
+ locale: string
+ }
+ }
+ ) {
+ const contentfulProducts = await this.deliveryClient.getEntries({
+ limit: 15,
+ content_type: "product",
+ "fields.medusaId": filter.id,
+ locale: filter.context?.locale,
+ include: 3,
+ })
+
+ return contentfulProducts.items.map((product) => {
+ // remove links
+ const { productVariants: _, productOptions: __, ...productFields } = product.fields
+ return {
+ ...productFields,
+ product_id: product.fields.medusaId,
+ variants: product.fields.productVariants.map((variant) => {
+ // remove circular reference
+ const { product: _, productOptionValues: __, ...variantFields } = variant.fields
+ return {
+ ...variantFields,
+ product_variant_id: variant.fields.medusaId,
+ options: variant.fields.productOptionValues.map((option) => {
+ // remove circular reference
+ const { productOption: _, ...optionFields } = option.fields
+ return {
+ ...optionFields,
+ product_option_id: option.fields.medusaId,
+ }
+ }),
+ }
+ }),
+ options: product.fields.productOptions.map((option) => {
+ // remove circular reference
+ const { product: _, ...optionFields } = option.fields
+ return {
+ ...optionFields,
+ product_option_id: option.fields.medusaId,
+ values: option.fields.values.map((value) => {
+ // remove circular reference
+ const { productOptionValue: _, ...valueFields } = value.fields
+ return {
+ ...valueFields,
+ product_option_value_id: value.fields.medusaId,
+ }
+ }),
+ }
+ }),
+ }
+ })
+ }
+}
+```
+
+You add a `list` method that accepts an object with the following properties:
+
+1. `id`: The ID of the product(s) in Medusa to retrieve their entries in Contentful.
+2. `context`: An object with the `locale` property that holds the locale code to retrieve the product's entry in Contentful for that locale.
+
+In the method, you use the Delivery API client's `getEntries` method to retrieve the products. You pass the following parameters:
+
+- `limit`: The maximum number of products to retrieve.
+- `content_type`: The content type of the entries to retrieve, which is `product`.
+- `fields.medusaId`: Filter the products by their `medusaId` field, which holds the ID of the product in Medusa.
+- `locale`: The locale code to retrieve the fields of the product in that locale.
+- `include`: The depth of the included nested entries. This ensures that you can retrieve the product's variants and options, and their values.
+
+Then, you format the retrieved products to:
+
+- Pass the product's ID in the `product_id` property. This is essential to map a product in Medusa to its entry in Contentful.
+- Remove the circular references in the product's variants, options, and values to avoid infinite loops.
+
+To paginate the retrieved products, implemet a `listAndCount` method as explained in the [Query Context](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query-context#using-pagination-with-query/index.html.md) documentation.
+
+### Retrieve Product Details for Locale API Route
+
+You'll now create the API route that returns a product's details for a given locale.
+
+You can create an API route that accepts path parameters by creating a directory within the route file's path whose name is of the format `[param]`.
+
+So, create the file `src/api/store/products/[id]/[locale]/route.ts` with the following content:
+
+```ts title="src/api/store/products/[id]/[locale]/route.ts" highlights={getProductLocaleDetailsRouteHighlights}
+import {
+ MedusaRequest,
+ MedusaResponse,
+} from "@medusajs/framework/http"
+import { QueryContext } from "@medusajs/framework/utils"
+
+export const GET = async (
+ req: MedusaRequest,
+ res: MedusaResponse
+) => {
+ const { locale, id } = req.params
+
+ const query = req.scope.resolve("query")
+
+ const { data } = await query.graph({
+ entity: "product",
+ fields: [
+ "id",
+ "contentful_product.*",
+ ],
+ filters: {
+ id,
+ },
+ context: {
+ contentful_product: QueryContext({
+ locale,
+ }),
+ },
+ })
+
+ res.json({
+ product: data[0],
+ })
+}
+```
+
+Since you export a `GET` route handler function, you expose a `GET` route at `/store/products/[id]/[locale]`. The route accepts two path parameters: the product's ID and the locale code.
+
+In the route handler, you retrieve the `locale` and `id` path parameters from the request. Then, you resolve [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) from the Medusa container.
+
+Next, you use Query to retrieve the localized details of the specified product. To do that, you pass an object with the following properties:
+
+- `entity`: The entity to retrieve, which is `product`.
+- `fields`: The fields to retrieve. Notice that you include the `contentful_product.*` field, which is available through the module link you created earlier.
+- `filters`: The filter to apply on the retrieved products. You apply the product's ID as a filter.
+- `context`: An additional context to be passed to the methods retrieving the data. To pass a context, you use [Query Context](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query-context/index.html.md).
+
+By specifying `contentful_product.*` in the `fields` property, Medusa will retrieve the product's entry from Contentful using the `list` method you added to the Contentful Module's service.
+
+Medusa passes the filters and context to the `list` method, and attaches the returned data to the Medusa product if its `product_id` matches the product's ID.
+
+Finally, you return the product's details in the JSON response.
+
+You can now use this route to retrieve a product's details for a given locale.
+
+### Show Localized Product Details in Storefront
+
+Now that you expose the localized product details, you can customize the storefront to show them.
+
+#### Install Contentful Rich Text Package
+
+When you retrieve the entries from Contentful, rich-text fields are returned as an object that requires special rendering. So, Contentful provides a package to render rich-text fields.
+
+Install the package by running the following command:
+
+```bash npm2yarn
+npm install @contentful/@contentful/rich-text-types
+```
+
+You'll use this package to render the product's description.
+
+#### Retrieve Localized Product Details Function
+
+To retrieve a product's details for a given locale, you'll add a function that sends a request to the API route you created.
+
+First, add the following import at the top of `src/lib/data/locale.ts`:
+
+```ts title="src/lib/data/locale.ts" badgeLabel="Storefront" badgeColor="blue"
+import type { Document } from "@contentful/rich-text-types"
+```
+
+Then, add the following type and function at the end of the file:
+
+```ts title="src/lib/data/locale.ts" badgeLabel="Storefront" badgeColor="blue"
+export type ProductLocaleDetails = {
+ id: string
+ contentful_product: {
+ product_id: string
+ title: string
+ handle: string
+ description: Document
+ subtitle?: string
+ variants: {
+ title: string
+ product_variant_id: string
+ options: {
+ value: string
+ product_option_id: string
+ }[]
+ }[]
+ options: {
+ title: string
+ product_option_id: string
+ values: {
+ title: string
+ product_option_value_id: string
+ }[]
+ }[]
+ }
+}
+
+export async function getProductLocaleDetails(
+ productId: string
+) {
+ const localeCode = await getSelectedLocale()
+
+ return await sdk.client.fetch<{
+ product: ProductLocaleDetails
+ }>(`/store/products/${productId}/${localeCode}`)
+}
+```
+
+You define a `ProductLocaleDetails` type that describes the structure of a localized product's details.
+
+You also define a `getProductLocaleDetails` function that sends a request to the API route you created and returns the localized product's details.
+
+#### Show Localized Product Title in Products Listing
+
+Next, you'll customize existing components to show the localized product details.
+
+The component defined in `src/modules/products/components/product-preview/index.tsx` shows the product's details in the products listing page. You need to retrieve the localized product details and show the product's title in the selected locale.
+
+In `src/modules/products/components/product-preview/index.tsx`, add the following import:
+
+```tsx title="src/modules/products/components/product-preview/index.tsx" badgeLabel="Storefront" badgeColor="blue"
+import { getProductLocaleDetails } from "@lib/data/locale"
+```
+
+Then, in the `ProductPreview` component in the same file, add the following before the `return` statement:
+
+```tsx title="src/modules/products/components/product-preview/index.tsx" badgeLabel="Storefront" badgeColor="blue"
+const productLocaleDetails = await getProductLocaleDetails(product.id!)
+```
+
+This will retrieve the localized product details for the selected locale.
+
+Finally, to show the localized product title, find in the `ProductPreview` component's `return` statement the following line:
+
+```tsx title="src/modules/products/components/product-preview/index.tsx" badgeLabel="Storefront" badgeColor="blue"
+{product.title}
+```
+
+And replace it with the following:
+
+```tsx title="src/modules/products/components/product-preview/index.tsx" badgeLabel="Storefront" badgeColor="blue"
+{productLocaleDetails.product.contentful_product?.title || product.title}
+```
+
+You'll test it out after the next step.
+
+#### Show Localized Product Details in Product Page
+
+Next, you'll customize the product page to show the localized product details.
+
+The product's details page is defined in `src/app/[countryCode]/(main)/products/[handle]/page.tsx`. So, add the following import at the top of the file:
+
+```tsx title="src/app/[countryCode]/(main)/products/[handle]/page.tsx" badgeLabel="Storefront" badgeColor="blue"
+import { getProductLocaleDetails } from "@lib/data/locale"
+```
+
+Then, in the `ProductPage` component in the same file, add the following before the `return` statement:
+
+```tsx title="src/app/[countryCode]/(main)/products/[handle]/page.tsx" badgeLabel="Storefront" badgeColor="blue"
+const productLocaleDetails = await getProductLocaleDetails(pricedProduct.id!)
+```
+
+This will retrieve the localized product details for the selected locale.
+
+Finally, in the `ProductPage` component in the same file, pass the following prop to `ProductTemplate`:
+
+```tsx title="src/app/[countryCode]/(main)/products/[handle]/page.tsx" badgeLabel="Storefront" badgeColor="blue"
+return (
+
+)
+```
+
+Next, you'll customize the `ProductTemplate` component to accept and use this prop.
+
+In `src/modules/products/templates/index.tsx`, add the following import:
+
+```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue"
+import { ProductLocaleDetails } from "@lib/data/locale"
+```
+
+Then, update the `ProductTemplateProps` type to include the `productLocaleDetails` prop:
+
+```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue"
+export type ProductTemplateProps = {
+ // ...
+ productLocaleDetails: ProductLocaleDetails
+}
+```
+
+Next, update the `ProductTemplate` component to destructure the `productLocaleDetails` prop:
+
+```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue"
+const ProductTemplate: React.FC = ({
+ // ...
+ productLocaleDetails,
+}) => {
+ // ...
+}
+```
+
+Finally, pass the `productLocaleDetails` prop to the `ProductInfo` component in the `return` statement:
+
+```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue"
+
+```
+
+The `ProductInfo` component shows the product's details. So, you need to update it to accept and use the `productLocaleDetails` prop.
+
+In `src/modules/products/templates/product-info/index.tsx`, add the following imports:
+
+```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue"
+import { ProductLocaleDetails } from "@lib/data/locale"
+import { documentToHtmlString } from "@contentful/rich-text-html-renderer"
+```
+
+Then, update the `ProductInfoProps` type to include the `productLocaleDetails` prop:
+
+```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue"
+export type ProductInfoProps = {
+ // ...
+ productLocaleDetails: ProductLocaleDetails
+}
+```
+
+Next, update the `ProductInfo` component to destructure the `productLocaleDetails` prop:
+
+```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue"
+const ProductInfo = ({ product, productLocaleDetails }: ProductInfoProps) => {
+ // ...
+}
+```
+
+Then, find the following line in the `return` statement:
+
+```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue"
+{product.title}
+```
+
+And replace it with the following:
+
+```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue"
+{productLocaleDetails.contentful_product?.title || product.title}
+```
+
+Also, find the following line:
+
+```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue"
+{product.description}
+```
+
+And replace it with the following:
+
+```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue"
+{productLocaleDetails.contentful_product?.description ?
+ :
+ product.description
+}
+```
+
+You use the `documentToHtmlString` function to render the rich-text field. The function returns an HTML string that you can use to render the description.
+
+### Test out the Localized Product Details
+
+You can now test out all the changes made in this step.
+
+To do that, start the Medusa application by running the following command in the Medusa application's directory:
+
+```bash npm2yarn
+npm run dev
+```
+
+Then, start the Next.js Starter Storefront by running the following command in the storefront's directory:
+
+```bash npm2yarn
+npm run dev
+```
+
+Open the storefront at `http://localhost:8000` and select a different locale.
+
+Then, open the products listing page by clicking on Menu -> Store. You'll see the product titles in the selected locale.
+
+
+
+Then, if you click on a product, you'll see the product's title and description in the selected locale.
+
+
+
+***
+
+## Step 8: Sync Changes from Contentful to Medusa
+
+The last feature you'll implement is syncing changes from Contentful to Medusa.
+
+Contentful's webhooks allow you to listen to changes in your Contentful entries. You can then set up a webhook listener API route in Medusa that updates the product's data.
+
+In this step, you'll set up a webhook listener that updates Medusa's product data when a Contentful entry is published.
+
+### Prerequisites: Public Server
+
+Webhooks can only trigger deployed listeners. So, you must either [deploy your Medusa application](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/deployment/index.html.md), or use tools like [ngrok](https://ngrok.com/) to publicly expose your local application.
+
+### Set Up Webhooks in Contentful
+
+Before setting up the webhook listener, you need to set up a webhook in Contentful. To do that, on the Contentful dashboard:
+
+1. Click on the cog icon at the top right, then choose "Webhooks" from the dropdown.
+
+
+
+2. On the Webhooks page, click on the "Add Webhook" button.
+3. In the webhook form:
+ - In the Name fields, enter a name, such as "Medusa".
+ - In the URL field, enter `{your_app}/hooks/contentful`, where `{your_app}` is the public URL of your Medusa application. You'll create the `/hooks/contentful` API route in a bit.
+ - In the Triggers section, select the "Published" trigger for "Entry".
+
+
+
+- Scroll down to the "Headers" section, and choose "application/json" for the "Content type" field.
+
+
+
+4. Once you're done, click the Save button.
+
+### Setup Webhook Secret in Contentful
+
+You also need to add a webhook secret in Contentful. To do that, on the Contentful dashboard:
+
+1. Click on the cog icon at the top right, then choose "Webhooks" from the dropdown.
+2. On the Webhooks page, click on the "Settings" tab.
+3. Click on the "Enable request verification" button.
+
+
+
+4. Copy the secret that shows up. You can update it later but you can't see the same secret again.
+
+You'll use the secret to verify the webhook request in Medusa next.
+
+### Update Contentful Module Options
+
+First, add the webhook secret as an environment variable in the Medusa application's `.env` file:
+
+```plain
+CONTENTFUL_WEBHOOK_SECRET=aEl7...
+```
+
+Next, add the webhook secret to the Contentful Module options in the Medusa application's `medusa-config.ts` file:
+
+```ts title="medusa-config.ts"
+module.exports = defineConfig({
+ // ...
+ modules: [
+ {
+ resolve: "./src/modules/contentful",
+ options: {
+ // ...
+ webhook_secret: process.env.CONTENTFUL_WEBHOOK_SECRET,
+ },
+ },
+ ],
+})
+```
+
+Finally, update the `ModuleOptions` type in `src/modules/contentful/loader/create-content-models.ts` to include the `webhook_secret` option:
+
+```ts title="src/modules/contentful/loader/create-content-models.ts"
+export type ModuleOptions = {
+ // ...
+ webhook_secret: string
+}
+```
+
+### Add Verify Request Method
+
+Next, you'll add a method to the Contentful Module's service that verifies a webhook request.
+
+To verify the request, you'll need the `@contentful/node-apps-toolkit` package that provides utility functions for Node.js applications.
+
+So, run the following command to install it:
+
+```bash npm2yarn
+npm install @contentful/node-apps-toolkit
+```
+
+Then, add the following method to the Contentful Module's service in `src/modules/contentful/service.ts`:
+
+```ts title="src/modules/contentful/service.ts"
+// other imports...
+import {
+ CanonicalRequest,
+ verifyRequest,
+} from "@contentful/node-apps-toolkit"
+
+export default class ContentfulModuleService {
+ // ...
+ async verifyWebhook(request: CanonicalRequest) {
+ if (!this.options.webhook_secret) {
+ throw new MedusaError(
+ MedusaError.Types.INVALID_DATA,
+ "Webhook secret is not set"
+ )
+ }
+ return verifyRequest(this.options.webhook_secret, request, 0)
+ }
+}
+```
+
+You add a `verifyWebhook` method that verifies a webhook request using the `verifyRequest` function.
+
+You pass to the `verifyRequest` function the webhook secret from the module's options with the request's details. You also disable time-to-live (TTL) check by passing `0` as the third argument.
+
+### Handle Contentful Webhook Workflow
+
+Before you add the webhook listener, the last piece you need is to add a workflow that handles the necessary updates based on the webhook event.
+
+The workflow will have the following steps:
+
+- [prepareUpdateDataStep](#prepareUpdateDataStep): Prepare the data for the update
+
+You only need to implement the first step, as Medusa provides the other workflows (running as steps) in the `@medusajs/medusa/core-flows` package.
+
+#### prepareUpdateDataStep
+
+The first step receives the webhook data payload and, based on the entry type, returns the data necessary for the update.
+
+To create the step, create the file `src/workflows/steps/prepare-update-data.ts` with the following content:
+
+```ts title="src/workflows/steps/prepare-update-data.ts" highlights={prepareUpdateDataStepHighlights}
+import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
+import { EntryProps } from "contentful-management"
+import ContentfulModuleService from "../../modules/contentful/service"
+import { CONTENTFUL_MODULE } from "../../modules/contentful"
+
+type StepInput = {
+ entry: EntryProps
+}
+
+export const prepareUpdateDataStep = createStep(
+ "prepare-update-data",
+ async ({ entry }: StepInput, { container }) => {
+ const contentfulModuleService: ContentfulModuleService =
+ container.resolve(CONTENTFUL_MODULE)
+
+ const defaultLocale = await contentfulModuleService.getDefaultLocaleCode()
+
+ let data: Record = {}
+
+ switch (entry.sys.contentType.sys.id) {
+ case "product":
+ data = {
+ id: entry.fields.medusaId[defaultLocale!],
+ title: entry.fields.title[defaultLocale!],
+ subtitle: entry.fields.subtitle?.[defaultLocale!] || undefined,
+ handle: entry.fields.handle[defaultLocale!],
+ }
+ break
+ case "productVariant":
+ data = {
+ id: entry.fields.medusaId[defaultLocale!],
+ title: entry.fields.title[defaultLocale!],
+ }
+ break
+ case "productOption":
+ data = {
+ selector: {
+ id: entry.fields.medusaId[defaultLocale!],
+ },
+ update: {
+ title: entry.fields.title[defaultLocale!],
+ },
+ }
+ break
+ }
+
+ return new StepResponse(data)
+ }
+)
+```
+
+You define a `prepareUpdateDataStep` function that receives the webhook data payload as an input.
+
+In the step, you resolve the Contentful Module's service and use it to retrieve the default locale code. You need it to find the value to update the fields in Medusa.
+
+Next, you prepare the data to return based on the entry type:
+
+- `product`: The product's ID, title, subtitle, and handle.
+- `productVariant`: The product variant's ID and title.
+- `productOption`: The product option's ID and title.
+
+The data is prepared based on the expected input for the workflows that will be used to update the data.
+
+#### Create the Workflow
+
+You can now create the workflow that handles the webhook event.
+
+Create the file `src/workflows/handle-contentful-hook.ts` with the following content:
+
+```ts title="src/workflows/handle-contentful-hook.ts" highlights={handleContentfulHookWorkflowHighlights} collapsibleLines="1-14" expandButtonLabel="Show Imports"
+import { createWorkflow, when } from "@medusajs/framework/workflows-sdk"
+import { EntryProps } from "contentful-management"
+import { prepareUpdateDataStep } from "./steps/prepare-update-data"
+import {
+ updateProductOptionsWorkflow,
+ updateProductsWorkflow,
+ updateProductVariantsWorkflow,
+ UpdateProductOptionsWorkflowInput,
+} from "@medusajs/medusa/core-flows"
+import {
+ UpsertProductDTO,
+ UpsertProductVariantDTO,
+} from "@medusajs/framework/types"
+
+export type WorkflowInput = {
+ entry: EntryProps
+}
+
+export const handleContentfulHookWorkflow = createWorkflow(
+ { name: "handle-contentful-hook-workflow" },
+ (input: WorkflowInput) => {
+ const prepareUpdateData = prepareUpdateDataStep({
+ entry: input.entry,
+ })
+
+ when(input, (input) => input.entry.sys.contentType.sys.id === "product")
+ .then(() => {
+ updateProductsWorkflow.runAsStep({
+ input: {
+ products: [prepareUpdateData as UpsertProductDTO],
+ },
+ })
+ })
+
+ when(input, (input) =>
+ input.entry.sys.contentType.sys.id === "productVariant"
+ )
+ .then(() => {
+ updateProductVariantsWorkflow.runAsStep({
+ input: {
+ product_variants: [prepareUpdateData as UpsertProductVariantDTO],
+ },
+ })
+ })
+
+ when(input, (input) =>
+ input.entry.sys.contentType.sys.id === "productOption"
+ )
+ .then(() => {
+ updateProductOptionsWorkflow.runAsStep({
+ input: prepareUpdateData as unknown as UpdateProductOptionsWorkflowInput,
+ })
+ })
+ }
+)
+```
+
+You define a `handleContentfulHookWorkflow` function that receives the webhook data payload as an input.
+
+In the workflow, you:
+
+- Prepare the data for the update using the `prepareUpdateDataStep` step.
+- Use a [when](https://docs.medusajs.com/docs/learn/fundamentals/workflows/conditions/index.html.md) condition to check if the entry type is a `product`, and if so, update the product using the `updateProductsWorkflow`.
+- Use a `when` condition to check if the entry type is a `productVariant`, and if so, update the product variant using the `updateProductVariantsWorkflow`.
+- Use a `when` condition to check if the entry type is a `productOption`, and if so, update the product option using the `updateProductOptionsWorkflow`.
+
+You can't perform data manipulation in a workflow's constructor function. Instead, the Workflows SDK includes utility functions like `when` to perform typical operations that requires accessing data values. Learn more about workflow constraints in the [Workflow Constraints](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md) documetation.
+
+### Add the Webhook Listener API Route
+
+You can finally add the API route that acts as a webhook listener.
+
+To add the API route, create the file `src/api/hooks/contentful/route.ts` with the following content:
+
+```ts title="src/api/hooks/contentful/route.ts" highlights={contentfulHookRouteHighlights} collapsibleLines="1-10" expandButtonLabel="Show Imports"
+import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
+import {
+ handleContentfulHookWorkflow,
+ HandleContentfulHookWorkflowInput,
+} from "../../../workflows/handle-contentful-hook"
+import { CONTENTFUL_MODULE } from "../../../modules/contentful"
+import { CanonicalRequest } from "@contentful/node-apps-toolkit"
+import { MedusaError } from "@medusajs/framework/utils"
+import ContentfulModuleService from "../../../modules/contentful/service"
+
+export const POST = async (
+ req: MedusaRequest,
+ res: MedusaResponse
+) => {
+ const contentfulModuleService: ContentfulModuleService =
+ req.scope.resolve(CONTENTFUL_MODULE)
+
+ const isValid = await contentfulModuleService.verifyWebhook({
+ path: req.path,
+ method: req.method.toUpperCase(),
+ headers: req.headers,
+ body: JSON.stringify(req.body),
+ } as unknown as CanonicalRequest)
+
+ if (!isValid) {
+ throw new MedusaError(
+ MedusaError.Types.UNAUTHORIZED,
+ "Invalid webhook request"
+ )
+ }
+
+ await handleContentfulHookWorkflow(req.scope).run({
+ input: {
+ entry: req.body,
+ } as unknown as HandleContentfulHookWorkflowInput,
+ })
+
+ res.status(200).send("OK")
+}
+```
+
+Since you export a `POST` route handler function, you expose a `POST` route at `/hooks/contentful`.
+
+In the route, you first use the `verifyWebhook` method of the Contentful Module's service to verify the request. If the request is invalid, you throw an error.
+
+Then, you run the `handleContentfulHookWorkflow` passing the request's body, which is the webhook data payload, as an input.
+
+Finally, you return a `200` response to Contentful to confirm that the webhook was received and processed.
+
+### Test the Webhook Listener
+
+To test out the webhook listener, start the Medusa application:
+
+```bash npm2yarn
+npm run dev
+```
+
+Then, try updating a product's title (in the default locale) in Contentful. You should see the product's title updated in Medusa.
+
+***
+
+## Next Steps
+
+You've now integrated Contentful with Medusa and supported localized product details. You can expand on the features in this tutorial to:
+
+1. Add support for other data types, such as product categories or collections.
+ - Refer to the data model references for each [Commerce Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md) to figure out the content types you need to create in Contentful.
+2. Listen to other product events and update the Contentful entries accordingly.
+ - Refer to the [Events Reference](https://docs.medusajs.com/references/events/index.html.md) for details on all events emitted in Medusa.
+3. Add localization for the entire Next.js Starter Storefront. You can either:
+ - Create content types in Contentful for different sections in the storefront, then use them to retrieve the localized content;
+ - Or use the approaches recommended in the [Next.js documentation](https://nextjs.org/docs/app/building-your-application/routing/internationalization).
+
+If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth learning of all the concepts you've used in this guide and more.
+
+To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).
+
+### Troubleshooting
+
+If you encounter issues during your development, check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/index.html.md).
+
+### Getting Help
+
+If you encounter issues not covered in the troubleshooting guides:
+
+1. Visit the [Medusa GitHub repository](https://github.com/medusajs/medusa) to report issues or ask questions.
+2. Join the [Medusa Discord community](https://discord.gg/medusajs) for real-time support from community members.
+3. Contact the [sales team](https://medusajs.com/contact/) to get help from the Medusa team.
+
+
# 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.
@@ -57187,6 +59987,681 @@ If you're new to Medusa, check out the [main documentation](https://docs.medusaj
To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).
+# Integrate Segment (Analytics) with Medusa
+
+In this tutorial, you'll learn how to integrate Segment with Medusa to track events and analytics.
+
+When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. Medusa's architecture facilitates integrating third-party services to customize Medusa's infrastructure for your business needs.
+
+To track analytics in your Medusa application, you can integrate [Segment](https://segment.com/), a service that collects analytics from multiple sources and sends them to various destinations. This tutorial will help you set up Segment in your Medusa application and track common events.
+
+## Summary
+
+By following this tutorial, you'll learn how to:
+
+- Install and set up Medusa.
+- Integrate Segment with your Medusa application.
+- Handle Medusa's `order.placed` event to track order placements.
+- Track custom events in your Medusa application with Segment.
+
+You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer.
+
+
+
+[Example Repository](https://github.com/medusajs/examples/tree/main/segment-integration): Find the full code of the guide in this repository.
+
+***
+
+## Step 1: Install a Medusa Application
+
+### Prerequisites
+
+- [Node.js v20+](https://nodejs.org/en/download)
+- [Git CLI tool](https://git-scm.com/downloads)
+- [PostgreSQL](https://www.postgresql.org/download/)
+
+Start by installing the Medusa application on your machine with the following command:
+
+```bash
+npx create-medusa-app@latest
+```
+
+First, you'll be asked for the project's name. Then, when prompted about installing the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose "Yes."
+
+Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name and the Next.js Starter Storefront in a separate directory named `{project-name}-storefront`.
+
+The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).
+
+Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterwards, you can log in with the new user and explore the dashboard.
+
+Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.
+
+***
+
+## Step 2: Create Segment Module Provider
+
+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's [Analytics Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/analytics/index.html.md) provides an interface to track events in your Medusa application. It delegates the actual tracking to the configured Analytics Module Provider.
+
+In this step, you'll integrate Segment as an Analytics Module Provider. Later, you'll use it to track events in your Medusa application.
+
+Refer to the [Modules](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) documentation to learn more about modules in Medusa.
+
+### a. Install Segment Node SDK
+
+Before you create the Segment Module Provider, you'll install the Segment Node SDK to interact with Segment's API.
+
+Run the following command in your Medusa application's directory:
+
+```bash npm2yarn
+npm install @segment/analytics-node
+```
+
+You'll use the SDK in the next steps.
+
+### b. Create Module Directory
+
+A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/segment`.
+
+### c. Create Segment Module's Service
+
+A module has a service that contains its logic. For Analytics Module Providers, the service implements the logic to track events in the third-party service.
+
+To create the service of the Segment Analytics Module Provider, create the file `src/modules/segment/service.ts` with the following content:
+
+```ts title="src/modules/segment/service.ts" highlights={serviceHighlights}
+import {
+ AbstractAnalyticsProviderService,
+ MedusaError,
+} from "@medusajs/framework/utils"
+import { Analytics } from "@segment/analytics-node"
+
+type Options = {
+ writeKey: string
+}
+
+type InjectedDependencies = {}
+
+class SegmentAnalyticsProviderService extends AbstractAnalyticsProviderService {
+ private client: Analytics
+ static identifier = "segment"
+
+ constructor(container: InjectedDependencies, options: Options) {
+ super()
+ if (!options.writeKey) {
+ throw new MedusaError(
+ MedusaError.Types.INVALID_DATA,
+ "Segment write key is required"
+ )
+ }
+ this.client = new Analytics({ writeKey: options.writeKey })
+ }
+}
+
+export default SegmentAnalyticsProviderService
+```
+
+An Analytics Module Provider's service must extend the `AbstractAnalyticsProviderService` class. It must also have an `identifier` static property with the unique identifier of the provider.
+
+A module provider's constructor receives two parameters:
+
+- `container`: The [module's container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md) that contains Framework resources available to the module. In this tutorial, you don't need to resolve any resources.
+- `options`: Options that are passed to the module provider when it's registered in Medusa's configurations. You define the following option:
+ - `writeKey`: The Segment write key. You'll learn how to retrieve and set this option in the [Add Module Provider to Medusa's Configurations](#h-add-module-provider-to-medusas-configurations) section.
+
+In the constructor, you create a Segment client using the Segment Node SDK. You pass the `writeKey` option to the client.
+
+You'll use this client to implement the service's methods in the next sections.
+
+Refer to the [Create Analytics Module Provider](https://docs.medusajs.com/references/analytics/provider/index.html.md) guide for detailed information about the methods.
+
+### d. Implement identify Method
+
+The `identify` method is used to identify a user in Segment. It associates the user's ID with their profile information, such as name and email.
+
+Add the `identify` method to the `SegmentAnalyticsProviderService` class:
+
+```ts title="src/modules/segment/service.ts"
+// other imports...
+import { ProviderIdentifyAnalyticsEventDTO } from "@medusajs/types"
+
+class SegmentAnalyticsProviderService extends AbstractAnalyticsProviderService {
+ // ...
+ async identify(data: ProviderIdentifyAnalyticsEventDTO): Promise {
+ const anonymousId = data.properties && "anonymousId" in data.properties ?
+ data.properties.anonymousId : undefined
+ const traits = data.properties && "traits" in data.properties ?
+ data.properties.traits : undefined
+
+ if ("group" in data) {
+ this.client.group({
+ groupId: data.group.id,
+ userId: data.actor_id,
+ anonymousId,
+ traits,
+ context: data.properties
+ })
+ } else {
+ this.client.identify({
+ userId: data.actor_id,
+ anonymousId,
+ traits,
+ context: data.properties
+ })
+ }
+ }
+}
+```
+
+#### Parameters
+
+The `identify` method receives an object with the following properties:
+
+- `actor_id`: The ID of the user being identified.
+- `group`: Alternatively, the group being identified. If this property is present, the `actor_id` is ignored.
+- `properties`: Additional properties to associate with the user or group. This can include traits like name, email, and so on.
+
+The method receives other parameters, which you can find in the [Create Analytics Module Provider](https://docs.medusajs.com/references/analytics/provider#identify/index.html.md) guide.
+
+#### Method Logic
+
+In the method, if the `group` property is present, you call the `group` method of the Segment client to identify a group. Otherwise, you call the `identify` method to identify a user.
+
+For both methods, you extract the `anonymousId` and `traits` from the `properties` object if they are present. You also pass the `actor_id` as the `userId`, and `group.id` for groups.
+
+### e. Implement track Method
+
+The `track` method is used to track events in Segment. It can track events like order placements, cart updates, and more.
+
+Add the `track` method to the `SegmentAnalyticsProviderService` class:
+
+```ts title="src/modules/segment/service.ts"
+// other imports...
+import { ProviderTrackAnalyticsEventDTO } from "@medusajs/types"
+
+class SegmentAnalyticsProviderService extends AbstractAnalyticsProviderService {
+ // ...
+ async track(data: ProviderTrackAnalyticsEventDTO): Promise {
+ const userId = "group" in data ?
+ data.actor_id || data.group?.id : data.actor_id
+ const anonymousId = data.properties && "anonymousId" in data.properties ?
+ data.properties.anonymousId : undefined
+
+ if (!userId && !anonymousId) {
+ throw new MedusaError(
+ MedusaError.Types.INVALID_DATA,
+ `Actor or group ID is required for event ${data.event}`
+ )
+ }
+
+ this.client.track({
+ userId,
+ anonymousId,
+ event: data.event,
+ properties: data.properties,
+ timestamp: data.properties && "timestamp" in data.properties ?
+ new Date(data.properties.timestamp) : undefined,
+ })
+ }
+}
+```
+
+#### Parameters
+
+The `track` method receives an object with the following properties:
+
+- `actor_id`: The ID of the user performing the event.
+- `group`: Alternatively, the group performing the event. If this property is present, the `actor_id` is ignored.
+- `event`: The name of the event being tracked.
+- `properties`: Additional properties associated with the event. This can include details like product ID, order ID, and so on.
+
+The method receives other parameters, which you can find in the [Create Analytics Module Provider](https://docs.medusajs.com/references/analytics/provider#track/index.html.md) guide.
+
+#### Method Logic
+
+In the method, you set the user ID either to the actor or group ID. You also check if the anonymous ID is present in the properties to use it.
+
+Next, you call the `track` method of the Segment client, passing it the user ID, anonymous ID, event name, properties, and timestamp (if present in the properties).
+
+### f. Implement shutdown Method
+
+The `shutdown` method is used to gracefully shut down the Segment client when the Medusa application is stopped. It allows you to send all pending events to Segment before the application exits.
+
+Add the following method to the `SegmentAnalyticsProviderService` class:
+
+```ts title="src/modules/segment/service.ts"
+class SegmentAnalyticsProviderService extends AbstractAnalyticsProviderService {
+ // ...
+ async shutdown(): Promise {
+ await this.client.flush({
+ close: true,
+ })
+ }
+}
+```
+
+#### Method Logic
+
+In the method, you call the `flush` method of the Segment client with the `close` option set to `true`. This method will send all pending events to Segment and close the client connection.
+
+### g. Export Module Definition
+
+You've now finished implementing the necessary methods for the Segment Analytics Module Provider.
+
+The final piece to a module is its definition, which you export in an `index.ts` file at the module's root directory. This definition tells Medusa the module's details, including its service.
+
+To create the module's definition, create the file `src/modules/segment/index.ts` with the following content:
+
+```ts title="src/modules/segment/index.ts"
+import SegmentAnalyticsProviderService from "./service"
+import {
+ ModuleProvider,
+ Modules,
+} from "@medusajs/framework/utils"
+
+export default ModuleProvider(Modules.ANALYTICS, {
+ services: [SegmentAnalyticsProviderService],
+})
+```
+
+You use `ModuleProvider` from the Modules SDK to create the module provider's definition. It accepts two parameters:
+
+1. The name of the module that this provider belongs to, which is `Modules.ANALYTICS` in this case.
+2. An object with a required property `services` indicating the Module Provider's services.
+
+### h. Add Module Provider to Medusa's Configurations
+
+Once you finish building the module, add it to Medusa's configurations to start using it.
+
+In `medusa-config.ts`, add a `modules` property:
+
+```ts title="medusa-config.ts"
+module.exports = defineConfig({
+ // ...
+ modules: [
+ {
+ resolve: "@medusajs/medusa/analytics",
+ options: {
+ providers: [
+ {
+ resolve: "./src/modules/segment",
+ id: "segment",
+ options: {
+ writeKey: process.env.SEGMENT_WRITE_KEY || "",
+ },
+ },
+ ],
+ },
+ },
+ ],
+})
+```
+
+To pass an Analytics Module Provider to the Analytics Module, you add the `modules` property to the Medusa configuration and pass the Analytics Module in its value.
+
+The Analytics Module accepts a `providers` option, which is an array of Analytics Module Providers to register. However, you can only register one analytics provider in your Medusa application.
+
+To register the Segment Analytics Module Provider, you add an object to the `providers` array with the following properties:
+
+- `resolve`: The NPM package or path to the module provider. In this case, it's the path to the `src/modules/segment` directory.
+- `id`: The ID of the module provider. The Analytics Module Provider is then registered with the ID `aly_{identifier}_{id}`, where:
+ - `{identifier}`: The identifier static property defined in the Module Provider's service, which is `segment` in this case.
+ - `{id}`: The ID set in this configuration, which is also `segment` in this case.
+- `options`: The options to pass to the module provider. These are the options you defined in the `Options` interface of the module provider's service.
+
+### i. Set Option as Environment Variable
+
+Next, you'll set the Segment write key as an environment variable.
+
+To retrieve the Segment write key:
+
+1. Log into your [Segment](https://app.segment.com) account.
+2. Go to the Connections page and click the "Add More" button next to the "Sources" section.
+
+
+
+3. In the "Choose a Source" step, select "Node.js" and click the "Next" button.
+
+
+
+4. In the "Connect your Node.js Source" step, enter a name for the source and click the "Create Source" button. This will show you the write key to copy.
+
+
+
+You can skip the next step of testing out the source for now.
+
+Then, add the following environment variable to your `.env` file:
+
+```shell
+SEGMENT_WRITE_KEY=123...
+```
+
+Replace `123...` with the write key you copied from Segment.
+
+You'll test out the integration as you set up event tracking in the next steps.
+
+***
+
+## Step 3: Track Order Placement Event
+
+You'll first track the order-placement event, which is triggered natively in the Medusa application.
+
+Medusa's events system allows you to listen to events triggered by the Medusa application and execute custom logic asynchronously in a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md).
+
+In the subscriber, you execute functionalities created in [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). A workflow is a series of actions, called steps, that complete a task.
+
+In this step, you'll create a workflow that tracks the `order.placed` event in Segment. Then, you'll create a subscriber that listens to this event and executes the workflow.
+
+### a. Create Track Event Step
+
+Before you create the workflow, you'll create a step that tracks an event in Segment. Later, you'll use this step in the workflows that track events, such as the order-placement event.
+
+To create a step, create the file `src/workflows/steps/track-event.ts` with the following content:
+
+```ts title="src/workflows/steps/track-event.ts" highlights={stepHighlights}
+import { createStep } from "@medusajs/framework/workflows-sdk"
+
+type TrackEventStepInput = {
+ event: string
+ userId?: string
+ properties?: Record
+ timestamp?: Date
+}
+
+export const trackEventStep = createStep(
+ "track-event",
+ async (input: TrackEventStepInput, { container }) => {
+ const analyticsModuleService = container.resolve(
+ "analytics"
+ )
+
+ if (!input.userId) {
+ // generate a random user id
+ input.properties = {
+ ...input.properties,
+ anonymousId: Math.random().toString(36).substring(2, 15) +
+ Math.random().toString(36).substring(2, 15),
+ }
+ }
+
+ await analyticsModuleService.track({
+ event: input.event,
+ actor_id: input.userId,
+ properties: input.properties,
+ })
+ }
+)
+```
+
+You create a step with `createStep` from the Workflows SDK. It accepts two parameters:
+
+1. The step's unique name, which is `track-event`.
+2. An async function that receives two parameters:
+ - The step's input, which is in this case an object with the following properties:
+ - `event`: The name of the event to track.
+ - `userId`: The ID of the user performing the event.
+ - `properties`: Additional properties associated with the event.
+ - `timestamp`: The timestamp of the event (optional).
+ - 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.
+
+The Medusa container is different from the module's container. Since modules are isolated, they each have a container with their resources. Refer to the [Module Container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md) documentation for more information.
+
+In the step function, you resolve the Analytics Module's service from the Medusa container. This service is the interface to track events with the configured Analytics Module Provider, which is Segment in this case.
+
+If the `userId` is not provided, you generate a random anonymous ID and add it to the properties. This is useful for tracking events from users who are not logged in.
+
+Finally, you call the `track` method of the Analytics Module's service, passing it the event name, user ID, and properties.
+
+### b. Create Track Order Placed Workflow
+
+Next, you'll create the workflow that tracks the order placement event.
+
+To create the workflow, create the file `src/workflows/track-order-placed.ts` with the following content:
+
+```ts title="src/workflows/track-order-placed.ts" highlights={workflowHighlights}
+import {
+ createWorkflow,
+ transform,
+} from "@medusajs/framework/workflows-sdk"
+import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
+import { trackEventStep } from "./steps/track-event"
+
+type WorkflowInput = {
+ id: string
+}
+
+export const trackOrderPlacedWorkflow = createWorkflow(
+ "track-order-placed",
+ ({ id }: WorkflowInput) => {
+ // @ts-ignore
+ const { data: orders } = useQueryGraphStep({
+ entity: "order",
+ fields: [
+ "id",
+ "email",
+ "total",
+ "currency_code",
+ "items.*",
+ "customer.id",
+ "customer.email",
+ "customer.first_name",
+ "customer.last_name",
+ "created_at",
+ ],
+ filters: {
+ id,
+ },
+ })
+
+ const order = transform({
+ order: orders[0],
+ }, ({ order }) => ({
+ orderId: order.id,
+ email: order.email,
+ total: order.total,
+ currency: order.currency_code,
+ items: order.items?.map((item) => ({
+ id: item?.id,
+ title: item?.title,
+ quantity: item?.quantity,
+ variant: item?.variant,
+ unit_price: item?.unit_price,
+ })),
+ customer: {
+ id: order.customer?.id,
+ email: order.customer?.email,
+ firstName: order.customer?.first_name,
+ lastName: order.customer?.last_name,
+ },
+ timestamp: order.created_at,
+ }))
+
+ trackEventStep({
+ event: "order.placed",
+ userId: order.customer?.id,
+ properties: order,
+ })
+ }
+)
+```
+
+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 an object holding the ID of the order placed.
+
+In the workflow's constructor function, you:
+
+1. Retrieve the Medusa order using the `useQueryGraphStep` helper step. This step uses Medusa's [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) tool to retrieve data across modules. You pass it the order ID to retrieve.
+2. Use [transform](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) to prepare the tracking data, as direct data and variable manipulation isn't allowed in workflows. Learn more in the [Data Manipulation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) documentation.
+3. Send the tracking event to Segment using the `trackEventStep` you created in the previous step.
+
+You now have the workflow that tracks the order placement event.
+
+Refer to the [Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) documentation to learn more about workflows and steps.
+
+### c. Handle order.placed Event
+
+Next, you'll create a subscriber that listens to the `order.placed` event and executes the workflow you created in the previous step.
+
+To create the subscriber, create the file `src/subscribers/order-placed.ts` with the following content:
+
+```ts title="src/subscribers/order-placed.ts" highlights={subscriberHighlights}
+import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
+import { trackOrderPlacedWorkflow } from "../workflows/track-order-placed"
+
+export default async function orderPlacedHandler({
+ event: { data },
+ container,
+}: SubscriberArgs<{ id: string }>) {
+ await trackOrderPlacedWorkflow(container)
+ .run({
+ input: {
+ id: data.id,
+ },
+ })
+}
+
+export const config: SubscriberConfig = {
+ event: "order.placed",
+}
+```
+
+A subscriber file must export:
+
+1. An asynchronous function, which is the subscriber that is executed when the event is emitted.
+2. A configuration object that holds the name of the event that the subscriber listens to, which is `order.placed` in this case.
+
+The subscriber function receives an object as a parameter that has the following properties:
+
+- `event`: An object that holds the event's data payload. The payload of the `order.placed` event is the ID of the order placed.
+- `container`: The Medusa container to access the Framework and commerce tools.
+
+In the subscriber function, you execute the `trackOrderPlacedWorkflow` by invoking it, passing the Medusa container as a parameter. Then, you chain a `run` method, passing it the order ID from the event's data payload as input.
+
+Refer to the [Events and Subscribers](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) documentation to learn more about creating subscribers.
+
+### Test it Out
+
+You'll now test out the segment integration by placing an order using the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md).
+
+The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.
+
+So, if your Medusa application's directory is `medusa-segment`, you can find the storefront by going back to the parent directory and changing to the `medusa-segment-storefront` directory:
+
+```bash
+cd ../medusa-segment-storefront # change based on your project name
+```
+
+First, run the following command in your Medusa application's directory to start the Medusa server:
+
+```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green"
+npm run dev
+```
+
+Then, run the following command in your Next.js Starter Storefront's directory to start the storefront:
+
+```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
+npm run dev
+```
+
+In the storefront, add a product to the cart and proceed to checkout. Once you place the order, open the Segment dashboard to view the order event:
+
+1. Go to Connections > Sources.
+2. Click on the Node.js source you created earlier.
+3. Click on the "Debugger" tab at the top of the page.
+4. You should see the `order.placed` event with the order details.
+
+The event may take a few seconds to appear in the debugger.
+
+
+
+***
+
+## Track Custom Event
+
+In your Medusa application, you often need to track custom events that are relevant to your business use case. For example, a B2B business may want to track whenever a user requests a quote.
+
+In Medusa, you can emit custom events in your workflows when an action occurs. Then, you can create a subscriber that listens to the custom event and executes a workflow to track it in Segment.
+
+For example, if you have a `createQuoteWorkflow`, you can use Medusa's [emitEventStep](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/emit-event#emit-event-in-a-workflow/index.html.md) to emit a custom event after the quote is created:
+
+```ts title="src/workflows/create-quote.ts"
+import {
+ createWorkflow,
+} from "@medusajs/framework/workflows-sdk"
+import {
+ emitEventStep,
+} from "@medusajs/medusa/core-flows"
+
+const createQuoteWorkflow = createWorkflow(
+ "create-quote",
+ () => {
+ // ...
+
+ emitEventStep({
+ eventName: "quote.created",
+ data: {
+ id: "123",
+ // other data payload
+ },
+ })
+ }
+)
+```
+
+You can then create a subscriber that listens to the `quote.created` event and executes a workflow to track it in Segment:
+
+```ts title="src/subscribers/quote-created.ts"
+import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
+import { trackQuoteWorkflow } from "../workflows/track-order-placed"
+
+export default async function orderPlacedHandler({
+ event: { data },
+ container,
+}: SubscriberArgs<{ id: string }>) {
+ await trackQuoteWorkflow(container)
+ .run({
+ input: {
+ id: data.id,
+ },
+ })
+}
+
+export const config: SubscriberConfig = {
+ event: "quote.created",
+}
+```
+
+The above example assumes you have a `trackQuoteWorkflow` that tracks the quote creation event in Segment, similar to the [trackOrderPlacedWorkflow](#b-create-track-order-placed-workflow) you created earlier.
+
+***
+
+## Next Steps
+
+You've now integrated Segment with your Medusa application and tracked common events like order placement. You can expand on the features in this tutorial to:
+
+- Track more events in your Medusa application, such as user sign-ups, cart additions, and more. You can refer to the [Events Reference](https://docs.medusajs.com/references/events/index.html.md) for a full list of events emitted by Medusa.
+- Emit custom events that are relevant for your business use case, and track them in Segment.
+- Add destinations to Segment to benefit from the data collected. Segment supports various destinations, such as Google Analytics, Metabase, and more.
+
+If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth understanding of all the concepts you've used in this guide and more.
+
+To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).
+
+### Troubleshooting
+
+If you encounter issues during your development, check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/index.html.md).
+
+### Getting Help
+
+If you encounter issues not covered in the troubleshooting guides:
+
+1. Visit the [Medusa GitHub repository](https://github.com/medusajs/medusa) to report issues or ask questions.
+2. Join the [Medusa Discord community](https://discord.gg/medusajs) for real-time support from community members.
+3. Contact the [sales team](https://medusajs.com/contact/) to get help from the Medusa team.
+
+
# Integrate Medusa with Sanity (CMS)
In this guide, you'll learn how to integrate Medusa with Sanity.
@@ -60240,3450 +63715,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 Localization in Medusa by Integrating Contentful
-
-In this tutorial, you'll learn how to localize your Medusa store's data with Contentful.
-
-When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. While Medusa provides features essential for internationalization, such as support for multiple [regions](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/region/index.html.md) and [currencies](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/currency/index.html.md), it doesn't provide content localization.
-
-However, Medusa's architecture supports the integration of third-party services to provide additional features, such as data localization. One service you can integrate is [Contentful](https://www.contentful.com/), a headless content management system (CMS) that allows you to manage and deliver content across multiple channels.
-
-## Summary
-
-By following this tutorial, you'll learn how to:
-
-- Install and set up Medusa.
-- Integrate Contentful with Medusa.
-- Create content types in Contentful for Medusa models.
-- Trigger syncing products and related data to Contentful when:
- - A product is created.
- - The admin user triggers syncing the products.
-- Customize the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) to fetch localized data from Contentful through Medusa.
-- Listen to webhook events in Contentful to update Medusa's data accordingly.
-
-You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer.
-
-
-
-- [Tutorial Repository](https://github.com/medusajs/examples/tree/main/localization-contentful): Find the full code for this guide in this repository.
-- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1744790686/OpenApi/Contentful_jysc07.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
-```
-
-First, you'll be asked for the project's name. Then, when prompted about installing the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose "Yes."
-
-Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name and the Next.js Starter Storefront in a separate directory named `{project-name}-storefront`.
-
-The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).
-
-Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterwards, you can log in with the new user and explore the dashboard.
-
-Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.
-
-***
-
-## Step 2: Create Contentful Module
-
-To integrate third-party services into Medusa, you create a module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a reusable package that provides 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 module that provides the necessary functionalities to integrate Contentful with Medusa.
-
-Refer to the [Modules](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) documentation to learn more about modules and their structure.
-
-### Install Contentful SDKs
-
-Before building the module, you need to install Contentful's management and delivery JS SDKs. So, run the following command in the Medusa application's directory:
-
-```bash npm2yarn
-npm install contentful contentful-management
-```
-
-Where `contentful` is the delivery SDK and `contentful-management` is the management SDK.
-
-### Create Module Directory
-
-A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/contentful`.
-
-### Create Loader
-
-When the Medusa application starts, you want to establish a connection to Contentful, then create the necessary content types if they don't exist in Contentful.
-
-A module can specify a task to run on the Medusa application's startup using [loaders](https://docs.medusajs.com/docs/learn/fundamentals/modules/loaders/index.html.md). A loader is an asynchronous function that a module exports. Then, when the Medusa application starts, it runs the loader. The loader can be used to perform one-time tasks such as connecting to a database, creating content types, or initializing data.
-
-Refer to the [Loaders](https://docs.medusajs.com/docs/learn/fundamentals/modules/loaders/index.html.md) documentation to learn more about how loaders work and when to use them.
-
-Loaders are created in a TypeScript or JavaScript file under the `loaders` directory of a module. So, create the file `src/modules/contentful/loader/create-content-models.ts` with the following content:
-
-```ts title="src/modules/contentful/loader/create-content-models.ts" highlights={loaderHighlights}
-import { LoaderOptions } from "@medusajs/framework/types"
-import { asValue } from "awilix"
-import { createClient } from "contentful-management"
-import { MedusaError } from "@medusajs/framework/utils"
-
-const { createClient: createDeliveryClient } = require("contentful")
-
-export type ModuleOptions = {
- management_access_token: string
- delivery_token: string
- space_id: string
- environment: string
- default_locale?: string
-}
-
-export default async function syncContentModelsLoader({
- container,
- options,
-}: LoaderOptions) {
- if (
- !options?.management_access_token || !options?.delivery_token ||
- !options?.space_id || !options?.environment
- ) {
- throw new MedusaError(
- MedusaError.Types.INVALID_DATA,
- "Contentful access token, space ID and environment are required"
- )
- }
-
- const logger = container.resolve("logger")
-
- try {
- const managementClient = createClient({
- accessToken: options.management_access_token,
- }, {
- type: "plain",
- defaults: {
- spaceId: options.space_id,
- environmentId: options.environment,
- },
- })
-
- const deliveryClient = createDeliveryClient({
- accessToken: options.delivery_token,
- space: options.space_id,
- environment: options.environment,
- })
-
-
- // TODO try to create content types
-
- } catch (error) {
- logger.error(
- `Failed to connect to Contentful: ${error}`
- )
- throw error
- }
-}
-```
-
-The loader file exports an asynchronous function that accepts an object having the following properties:
-
-- `container`: The [Module container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md), which is a registry of resources available to the module. You can use it to resolve or register resources in the module's container.
-- `options`: An object of options passed to the module. These options are useful to pass secrets or options that may change per environment. You'll learn how to pass these options later.
- - The Contentful Module expects the options to include the Contentful tokens for the management and delivery APIs, the space ID, environment, and optionally the default locale to use.
-
-In the loader function, you validate the options passed to the module, and throw an error if they're invalid. Then, you resolve from the Module's container the [Logger](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md) used to log messages in the terminal.
-
-Finally, you create clients for Contentful's management and delivery APIs, passing them the necessary module's options. If the connection fails, an error is thrown, which is handled in the `catch` block.
-
-#### Create Content Types
-
-In the loader, you need to create content types in Contentful if they don't already exist.
-
-In this tutorial, you'll only create content types for a product and its variants and options. However, you can create content types for other data models, such as categories or collections, by following the same approach.
-
-You can learn more about the product-related data models, which the content types are based on, in the [Product Module's Data Models](https://docs.medusajs.com/references/product/models/index.html.md) reference.
-
-To create the content type for products, replace the `TODO` in the loader with the following:
-
-```ts title="src/modules/contentful/loader/create-content-models.ts"
-// Try to create the product content type
-try {
- await managementClient.contentType.get({
- contentTypeId: "product",
- })
-} catch (error) {
- const productContentType = await managementClient.contentType.createWithId({
- contentTypeId: "product",
- }, {
- name: "Product",
- description: "Product content type synced from Medusa",
- displayField: "title",
- fields: [
- {
- id: "title",
- name: "Title",
- type: "Symbol",
- required: true,
- localized: true,
- },
- {
- id: "handle",
- name: "Handle",
- type: "Symbol",
- required: true,
- localized: false,
- },
- {
- id: "medusaId",
- name: "Medusa ID",
- type: "Symbol",
- required: true,
- localized: false,
- },
- {
- type: "RichText",
- name: "description",
- id: "description",
- validations: [
- {
- enabledMarks: [
- "bold",
- "italic",
- "underline",
- "code",
- "superscript",
- "subscript",
- "strikethrough",
- ],
- },
- {
- enabledNodeTypes: [
- "heading-1",
- "heading-2",
- "heading-3",
- "heading-4",
- "heading-5",
- "heading-6",
- "ordered-list",
- "unordered-list",
- "hr",
- "blockquote",
- "embedded-entry-block",
- "embedded-asset-block",
- "table",
- "asset-hyperlink",
- "embedded-entry-inline",
- "entry-hyperlink",
- "hyperlink",
- ],
- },
- {
- nodes: {},
- },
- ],
- localized: true,
- required: true,
- },
- {
- type: "Symbol",
- name: "subtitle",
- id: "subtitle",
- localized: true,
- required: false,
- validations: [],
- },
- {
- type: "Array",
- items: {
- type: "Link",
- linkType: "Asset",
- validations: [],
- },
- name: "images",
- id: "images",
- localized: true,
- required: false,
- validations: [],
- },
- {
- id: "productVariants",
- name: "Product Variants",
- type: "Array",
- localized: false,
- required: false,
- items: {
- type: "Link",
- validations: [
- {
- linkContentType: ["productVariant"],
- },
- ],
- linkType: "Entry",
- },
- disabled: false,
- omitted: false,
- },
- {
- id: "productOptions",
- name: "Product Options",
- type: "Array",
- localized: false,
- required: false,
- items: {
- type: "Link",
- validations: [
- {
- linkContentType: ["productOption"],
- },
- ],
- linkType: "Entry",
- },
- disabled: false,
- omitted: false,
- },
- ],
- })
-
- await managementClient.contentType.publish({
- contentTypeId: "product",
- }, productContentType)
-}
-
-// TODO create product variant content type
-```
-
-In the above snippet, you first try to retrieve the product content type using Contentful's Management APIs. If the content type doesn't exist, an error is thrown, which you handle in the `catch` block.
-
-In the `catch` block, you create the product content type with the following fields:
-
-- `title`: The product's title, which is a localized field.
-- `handle`: The product's handle, which is used to create a human-readable URL for the product in the storefront.
-- `medusaId`: The product's ID in Medusa, which is a non-localized field. You'll store in this field the ID of the product in Medusa.
-- `description`: The product's description, which is a localized rich-text field.
-- `subtitle`: The product's subtitle, which is a localized field.
-- `images`: The product's images, which is a localized array of assets in Contentful.
-- `productVariants`: The product's variants, which is an array that references content of the `productVariant` content type.
-- `productOptions`: The product's options, which is an array that references content of the `productOption` content type.
-
-Next, you'll create the `productVariant` content type that represents a product's variant. A variant is a combination of the product's options that customers can purchase. For example, a "red" shirt is a variant whose color option is `red`.
-
-To create the variant content type, replace the new `TODO` with the following:
-
-```ts title="src/modules/contentful/loader/create-content-models.ts"
-// Try to create the product variant content type
-try {
- await managementClient.contentType.get({
- contentTypeId: "productVariant",
- })
-} catch (error) {
- const productVariantContentType = await managementClient.contentType.createWithId({
- contentTypeId: "productVariant",
- }, {
- name: "Product Variant",
- description: "Product variant content type synced from Medusa",
- displayField: "title",
- fields: [
- {
- id: "title",
- name: "Title",
- type: "Symbol",
- required: true,
- localized: true,
- },
- {
- id: "product",
- name: "Product",
- type: "Link",
- required: true,
- localized: false,
- validations: [
- {
- linkContentType: ["product"],
- },
- ],
- disabled: false,
- omitted: false,
- linkType: "Entry",
- },
- {
- id: "medusaId",
- name: "Medusa ID",
- type: "Symbol",
- required: true,
- localized: false,
- },
- {
- id: "productOptionValues",
- name: "Product Option Values",
- type: "Array",
- localized: false,
- required: false,
- items: {
- type: "Link",
- validations: [
- {
- linkContentType: ["productOptionValue"],
- },
- ],
- linkType: "Entry",
- },
- disabled: false,
- omitted: false,
- },
- ],
- })
-
- await managementClient.contentType.publish({
- contentTypeId: "productVariant",
- }, productVariantContentType)
-}
-
-// TODO create product option content type
-```
-
-In the above snippet, you create the `productVariant` content type with the following fields:
-
-- `title`: The product variant's title, which is a localized field.
-- `product`: References the `product` content type, which is the product that the variant belongs to.
-- `medusaId`: The product variant's ID in Medusa, which is a non-localized field. You'll store in this field the ID of the variant in Medusa.
-- `productOptionValues`: The product variant's option values, which is an array that references content of the `productOptionValue` content type.
-
-Then, you'll create the `productOption` content type that represents a product's option, like size or color. Replace the new `TODO` with the following:
-
-```ts title="src/modules/contentful/loader/create-content-models.ts"
-// Try to create the product option content type
-try {
- await managementClient.contentType.get({
- contentTypeId: "productOption",
- })
-} catch (error) {
- const productOptionContentType = await managementClient.contentType.createWithId({
- contentTypeId: "productOption",
- }, {
- name: "Product Option",
- description: "Product option content type synced from Medusa",
- displayField: "title",
- fields: [
- {
- id: "title",
- name: "Title",
- type: "Symbol",
- required: true,
- localized: true,
- },
- {
- id: "product",
- name: "Product",
- type: "Link",
- required: true,
- localized: false,
- validations: [
- {
- linkContentType: ["product"],
- },
- ],
- disabled: false,
- omitted: false,
- linkType: "Entry",
- },
- {
- id: "medusaId",
- name: "Medusa ID",
- type: "Symbol",
- required: true,
- localized: false,
- },
- {
- id: "values",
- name: "Values",
- type: "Array",
- required: false,
- localized: false,
- items: {
- type: "Link",
- validations: [
- {
- linkContentType: ["productOptionValue"],
- },
- ],
- linkType: "Entry",
- },
- disabled: false,
- omitted: false,
- },
- ],
- })
-
- await managementClient.contentType.publish({
- contentTypeId: "productOption",
- }, productOptionContentType)
-}
-
-// TODO create product option value content type
-```
-
-In the above snippet, you create the `productOption` content type with the following fields:
-
-- `title`: The product option's title, which is a localized field.
-- `product`: References the `product` content type, which is the product that the option belongs to.
-- `medusaId`: The product option's ID in Medusa, which is a non-localized field. You'll store in this field the ID of the option in Medusa.
-- `values`: The product option's values, which is an array that references content of the `productOptionValue` content type.
-
-Finally, you'll create the `productOptionValue` content type that represents a product's option value, like "red" or "blue" for the color option. A variant references option values.
-
-To create the option value content type, replace the new `TODO` with the following:
-
-```ts title="src/modules/contentful/loader/create-content-models.ts"
-// Try to create the product option value content type
-try {
- await managementClient.contentType.get({
- contentTypeId: "productOptionValue",
- })
-} catch (error) {
- const productOptionValueContentType = await managementClient.contentType.createWithId({
- contentTypeId: "productOptionValue",
- }, {
- name: "Product Option Value",
- description: "Product option value content type synced from Medusa",
- displayField: "value",
- fields: [
- {
- id: "value",
- name: "Value",
- type: "Symbol",
- required: true,
- localized: true,
- },
- {
- id: "medusaId",
- name: "Medusa ID",
- type: "Symbol",
- required: true,
- localized: false,
- },
- ],
-})
-
-await managementClient.contentType.publish({
- contentTypeId: "productOptionValue",
- }, productOptionValueContentType)
-}
-
-// TODO register clients in container
-```
-
-In the above snippet, you create the `productOptionValue` content type with the following fields:
-
-- `value`: The product option value, which is a localized field.
-- `medusaId`: The product option value's ID in Medusa, which is a non-localized field. You'll store in this field the ID of the option value in Medusa.
-
-You've now created all the necessary content types to localize products.
-
-### Register Clients in the Container
-
-The last step in the loader is to register the Contentful management and delivery clients in the module's container. This will allow you to resolve and use them in the module's service, which you'll create next.
-
-To register resources in the container, you can use its `register` method, which accepts an object containing key-value pairs. The keys are the names of the resources in the container, and the values are the resources themselves.
-
-To register the management and delivery clients, replace the last `TODO` in the loader with the following:
-
-```ts title="src/modules/contentful/loader/create-content-models.ts"
-container.register({
- contentfulManagementClient: asValue(managementClient),
- contentfulDeliveryClient: asValue(deliveryClient),
-})
-
-logger.info("Connected to Contentful")
-```
-
-Now, you can resolve the management and delivery clients from the module's container using the keys `contentfulManagementClient` and `contentfulDeliveryClient`, respectively.
-
-### Create Service
-
-You define a module's functionality 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 perform actions with a third-party service.
-
-In this section, you'll create the Contentful Module's service that can be used to retrieve content from Contentful, create content, and more.
-
-To create the service, create the file `src/modules/contenful/service.ts` with the following content:
-
-```ts title="src/modules/contentful/service.ts" highlights={serviceHighlights}
-import { ModuleOptions } from "./loader/create-content-models"
-import { PlainClientAPI } from "contentful-management"
-
-type InjectedDependencies = {
- contentfulManagementClient: PlainClientAPI;
- contentfulDeliveryClient: any;
-}
-
-export default class ContentfulModuleService {
- private managementClient: PlainClientAPI
- private deliveryClient: any
- private options: ModuleOptions
-
- constructor(
- {
- contentfulManagementClient,
- contentfulDeliveryClient,
- }: InjectedDependencies,
- options: ModuleOptions
- ) {
- this.managementClient = contentfulManagementClient
- this.deliveryClient = contentfulDeliveryClient
- this.options = {
- ...options,
- default_locale: options.default_locale || "en-US",
- }
- }
-
- // TODO add methods
-}
-```
-
-You export a class that will be the Contentful Module's main service. In the class, you define properties for the Contentful clients and options passed to the module.
-
-You also add a constructor to the class. A service's constructor accepts the following params:
-
-1. The module's container, which you can use to resolve resources. You use it to resolve the Contentful clients you previously registered in the loader.
-2. The options passed to the module.
-
-In the constructor, you assign the clients and options to the class properties. You also set the default locale to `en-US` if it's not provided in the module's options.
-
-Since the loader is executed on application start-up, if an error occurs while connecting to Contentful, the module will not be registered and the service will not be executed. So, in the service, you're guaranteed that the clients are registered in the container and have successful connection to Contentful.
-
-As you implement the syncing and content retrieval features later, you'll add the necessary methods for them.
-
-### Export Module Definition
-
-The final piece to a module is its definition, which you export in an `index.ts` file at the module's root directory. This definition tells Medusa the name of the module, its service, and optionally its loaders.
-
-To create the module's definition, create the file `src/modules/contentful/index.ts` with the following content:
-
-```ts title="src/modules/contentful/index.ts" highlights={moduleHighlights}
-import { Module } from "@medusajs/framework/utils"
-import ContentfulModuleService from "./service"
-import createContentModelsLoader from "./loader/create-content-models"
-
-export const CONTENTFUL_MODULE = "contentful"
-
-export default Module(CONTENTFUL_MODULE, {
- service: ContentfulModuleService,
- loaders: [
- createContentModelsLoader,
- ],
-})
-```
-
-You use `Module` from the Modules SDK to create the module's definition. It accepts two parameters:
-
-1. The module's name, which is `contentful`.
-2. An object with a required property `service` indicating the module's service. You also pass the loader you created to ensure it's executed when the application starts.
-
-Aside from the module definition, you export the module's name as `CONTENTFUL_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/contentful",
- options: {
- management_access_token: process.env.CONTENTFUL_MANAGEMNT_ACCESS_TOKEN,
- delivery_token: process.env.CONTENTFUL_DELIVERY_TOKEN,
- space_id: process.env.CONTENTFUL_SPACE_ID,
- environment: process.env.CONTENTFUL_ENVIRONMENT,
- default_locale: "en-US",
- },
- },
- ],
-})
-```
-
-Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package's name.
-
-You also pass an `options` property with the module's options, including the Contentful's tokens for the management and delivery APIs, the Contentful's space ID, environment, and default locale.
-
-### Note about Locales
-
-By default, your Contentful space will have one locale (for example, `en-US`). You can add locales as explained in the [Contentful documentation](https://www.contentful.com/help/localization/manage-locales/).
-
-When you add a locale, make sure to:
-
-- Set the fallback locale to the default locale (for example, `en-US`). This ensure that values are retrieved in the default locale if values for the requested locale are not available.
-- Allow the required fields to be empty for the locale. Otherwise, you'll have to specify the values for the localized fields in each locale when you create the products later.
-
-
-
-### Add Environment Variables
-
-Before you can start using the Contentful Module, you need to add the necessary environment variables used in the module's options.
-
-Add the following environment variables to your `.env` file:
-
-```plain
-CONTENTFUL_MANAGEMNT_ACCESS_TOKEN=CFPAT-...
-CONTENTFUL_DELIVERY_TOKEN=eij...
-CONTENTFUL_SPACE_ID=t2a...
-CONTENTFUL_ENVIRONMENT=master
-```
-
-Where:
-
-- `CONTENTFUL_MANAGEMNT_ACCESS_TOKEN`: The Contentful management API access token. To create it on the Contentful dashboard:
- - Click on the cog icon at the top right, then choose "CMA tokens" from the dropdown.
-
-
-
-- In the CMA tokens page, click on the "Create personal access token" button.
-- In the window that pops up, enter a name for the token, and choose an expiry date. Once you're done, click the Generate button.
-- The token is generated and shown in the pop-up. Make sure to copy it and use it in the `.env` file, as you can't access it again.
-
-
-
-- `CONTENTFUL_DELIVERY_TOKEN`: An API token that you can use with the delivery API. To create it on the Contentful dashboard:
- - Click on the cog icon at the top right, then choose "API keys" from the dropdown.
-
-
-
-- In the APIs page, click on the "Add API key" button.
-- In the window that pops up, enter a name for the token, then click the Add API Key button.
-- This will create an API key and opens its page. On its page, copy the token for the "Content Delivery API" and use it as the value for `CONTENTFUL_DELIVERY_TOKEN`.
-
-
-
-- `CONTENTFUL_SPACE_ID`: The ID of your Contentful space. You can copy this from the dashboard's URL which is of the format `https://app.contentful.com/spaces/{space_id}/...`.
-- `CONTENTFUL_ENVIRONMENT`: The environment to manage and retrieve the content in. By default, you have the `master` environment which you can use. However, you can use another Contentful environment that you've created.
-
-Your module is now ready for use.
-
-### Test the Module
-
-To test out the module, you'll start the Medusa application, which will run the module's loader.
-
-To start the Medusa application, run the following command:
-
-```bash npm2yarn
-npm run dev
-```
-
-If the loader ran successfully, you'll see the following message in the terminal:
-
-```bash
-info: Connected to Contentful
-```
-
-You can also see on the Contentful dashboard that the content types were created. To view them, go to the Content Model page.
-
-
-
-***
-
-## Step 3: Create Products in Contentful
-
-Now that you have the Contentful Module ready for use, you can start creating products in Contentful.
-
-In this step, you'll implement the logic to create products in Contentful. Later, you'll execute it when:
-
-- A product is created in Medusa.
-- The admin user triggers a sync manually.
-
-### Add Methods to Contentful Module Service
-
-To create products in Contentful, you need to add the necessary methods in the Contentful Module's service. Then, you can use these methods later when building the creation flow.
-
-To create a product in Contentful, you'll need three methods: One to create the product's variants, another to create the product's options and values, and a third to create the product.
-
-In the service at `src/modules/contentful/service.ts`, start by adding the method to create the product's variants:
-
-```ts title="src/modules/contentful/service.ts"
-// imports...
-import { ProductVariantDTO } from "@medusajs/framework/types"
-import { EntryProps } from "contentful-management"
-
-export default class ContentfulModuleService {
- // ...
-
- private async createProductVariant(
- variants: ProductVariantDTO[],
- productEntry: EntryProps
- ) {
- for (const variant of variants) {
- await this.managementClient.entry.createWithId(
- {
- contentTypeId: "productVariant",
- entryId: variant.id,
- },
- {
- fields: {
- medusaId: {
- [this.options.default_locale!]: variant.id,
- },
- title: {
- [this.options.default_locale!]: variant.title,
- },
- product: {
- [this.options.default_locale!]: {
- sys: {
- type: "Link",
- linkType: "Entry",
- id: productEntry.sys.id,
- },
- },
- },
- productOptionValues: {
- [this.options.default_locale!]: variant.options.map((option) => ({
- sys: {
- type: "Link",
- linkType: "Entry",
- id: option.id,
- },
- })),
- },
- },
- }
- )
- }
- }
-}
-```
-
-You define a private method `createProductVariant` that accepts two parameters:
-
-1. The product's variants to create in Contentful.
-2. The product's entry in Contentful.
-
-In the method, you iterate over the product's variants and create a new entry in Contentful for each variant. You set the fields based on the product variant content type you created earlier.
-
-For each field, you specify the value for the default locale. In the Contentful dashboard, you can manage the values for other locales.
-
-Next, add the method to create the product's options and values:
-
-```ts title="src/modules/contentful/service.ts" highlights={createProductOptionHighlights}
-// other imports...
-import { ProductOptionDTO } from "@medusajs/framework/types"
-
-export default class ContentfulModuleService {
- // ...
- private async createProductOption(
- options: ProductOptionDTO[],
- productEntry: EntryProps
- ) {
- for (const option of options) {
- const valueIds: {
- sys: {
- type: "Link",
- linkType: "Entry",
- id: string
- }
- }[] = []
- for (const value of option.values) {
- await this.managementClient.entry.createWithId(
- {
- contentTypeId: "productOptionValue",
- entryId: value.id,
- },
- {
- fields: {
- value: {
- [this.options.default_locale!]: value.value,
- },
- medusaId: {
- [this.options.default_locale!]: value.id,
- },
- },
- }
- )
- valueIds.push({
- sys: {
- type: "Link",
- linkType: "Entry",
- id: value.id,
- },
- })
- }
- await this.managementClient.entry.createWithId(
- {
- contentTypeId: "productOption",
- entryId: option.id,
- },
- {
- fields: {
- medusaId: {
- [this.options.default_locale!]: option.id,
- },
- title: {
- [this.options.default_locale!]: option.title,
- },
- product: {
- [this.options.default_locale!]: {
- sys: {
- type: "Link",
- linkType: "Entry",
- id: productEntry.sys.id,
- },
- },
- },
- values: {
- [this.options.default_locale!]: valueIds,
- },
- },
- }
- )
- }
- }
-}
-```
-
-You define a private method `createProductOption` that accepts two parameters:
-
-1. The product's options, which is an array of objects.
-2. The product's entry in Contentful, which is an object.
-
-In the method, you iterate over the product's options and create entries for each of its values. Then, you create an entry for the option, and reference the values you created in Contentful. You set the fields based on the option and value content types you created earlier.
-
-Finally, add the method to create the product:
-
-```ts title="src/modules/contentful/service.ts" highlights={createProductHighlights}
-// other imports...
-import { ProductDTO } from "@medusajs/framework/types"
-
-export default class ContentfulModuleService {
- // ...
- async createProduct(
- product: ProductDTO
- ) {
- try {
- // check if product already exists
- const productEntry = await this.managementClient.entry.get({
- environmentId: this.options.environment,
- entryId: product.id,
- })
-
- return productEntry
- } catch(e) {}
-
- // Create product entry in Contentful
- const productEntry = await this.managementClient.entry.createWithId(
- {
- contentTypeId: "product",
- entryId: product.id,
- },
- {
- fields: {
- medusaId: {
- [this.options.default_locale!]: product.id,
- },
- title: {
- [this.options.default_locale!]: product.title,
- },
- description: product.description ? {
- [this.options.default_locale!]: {
- nodeType: "document",
- data: {},
- content: [
- {
- nodeType: "paragraph",
- data: {},
- content: [
- {
- nodeType: "text",
- value: product.description,
- marks: [],
- data: {},
- },
- ],
- },
- ],
- },
- } : undefined,
- subtitle: product.subtitle ? {
- [this.options.default_locale!]: product.subtitle,
- } : undefined,
- handle: product.handle ? {
- [this.options.default_locale!]: product.handle,
- } : undefined,
- },
- }
- )
-
- // Create options if they exist
- if (product.options?.length) {
- await this.createProductOption(product.options, productEntry)
- }
-
- // Create variants if they exist
- if (product.variants?.length) {
- await this.createProductVariant(product.variants, productEntry)
- }
-
- // update product entry with variants and options
- await this.managementClient.entry.update(
- {
- entryId: productEntry.sys.id,
- },
- {
- sys: productEntry.sys,
- fields: {
- ...productEntry.fields,
- productVariants: {
- [this.options.default_locale!]: product.variants?.map((variant) => ({
- sys: {
- type: "Link",
- linkType: "Entry",
- id: variant.id,
- },
- })),
- },
- productOptions: {
- [this.options.default_locale!]: product.options?.map((option) => ({
- sys: {
- type: "Link",
- linkType: "Entry",
- id: option.id,
- },
- })),
- },
- },
- }
- )
-
- return productEntry
- }
-}
-```
-
-You define a public method `createProduct` that accepts a product object as a parameter.
-
-In the method, you first check if the product already exists in Contentful. If it does, you return the existing product entry. Otherwise, you create a new product entry with the fields based on the product content type you created earlier.
-
-Next, you create entries for the product's options and variants using the methods you created earlier.
-
-Finally, you update the product entry to reference the variants and options you created.
-
-You now have all the methods to create products in Contentful. You'll also need one last method to delete a product in Contentful. This is useful when you implement the rollback mechanism in the flow that creates the products.
-
-Add the following method to the service:
-
-```ts title="src/modules/contentful/service.ts" highlights={deleteProductHighlights}
-// other imports...
-import { MedusaError } from "@medusajs/framework/utils"
-
-export default class ContentfulModuleService {
- // ...
- async deleteProduct(productId: string) {
- try {
- // Get the product entry
- const productEntry = await this.managementClient.entry.get({
- environmentId: this.options.environment,
- entryId: productId,
- })
-
- if (!productEntry) {
- return
- }
-
- // Delete the product entry
- await this.managementClient.entry.unpublish({
- environmentId: this.options.environment,
- entryId: productId,
- })
-
- await this.managementClient.entry.delete({
- environmentId: this.options.environment,
- entryId: productId,
- })
-
- // Delete the product variant entries
- for (const variant of productEntry.fields.productVariants[this.options.default_locale!]) {
- await this.managementClient.entry.unpublish({
- environmentId: this.options.environment,
- entryId: variant.sys.id,
- })
-
- await this.managementClient.entry.delete({
- environmentId: this.options.environment,
- entryId: variant.sys.id,
- })
- }
-
- // Delete the product options entries and values
- for (const option of productEntry.fields.productOptions[this.options.default_locale!]) {
- for (const value of option.fields.values[this.options.default_locale!]) {
- await this.managementClient.entry.unpublish({
- environmentId: this.options.environment,
- entryId: value.sys.id,
- })
-
- await this.managementClient.entry.delete({
- environmentId: this.options.environment,
- entryId: value.sys.id,
- })
- }
-
- await this.managementClient.entry.unpublish({
- environmentId: this.options.environment,
- entryId: option.sys.id,
- })
-
- await this.managementClient.entry.delete({
- environmentId: this.options.environment,
- entryId: option.sys.id,
- })
- }
- } catch (error) {
- throw new MedusaError(
- MedusaError.Types.INVALID_DATA,
- `Failed to delete product from Contentful: ${error.message}`
- )
- }
- }
-}
-```
-
-You define a public method `deleteProduct` that accepts a product ID as a parameter.
-
-In the method, you retrieve the product entry from Contentful with its variants, options, and values. For each entry, you must unpublish and delete it.
-
-You now have all the methods necessary to build the creation flow.
-
-### Create Contentful Product Workflow
-
-To implement the logic that's triggered when a product is created in Medusa, or when the admin user triggers a sync manually, you need to create a workflow.
-
-A [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) is a series of actions, called steps, that complete a task. You construct a workflow like you construct a function, but it's a special function that allows you to track its executions' progress, define roll-back logic, and configure other advanced features.
-
-Learn more about workflows in the [Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).
-
-In this section, you'll create a workflow that creates Medusa products in Contentful using the Contentful Module.
-
-The workflow has the following steps:
-
-- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve products to create in Contentful.
-- [createProductsContentfulStep](#createProductsContentfulStep): Create the products in Contentful.
-
-Medusa provides the `useQueryGraphStep` in its `@medusajs/medusa/core-flows` package. So, you only need to implement the second step.
-
-#### createProductsContentfulStep
-
-In the second step, you create the retrieved products in Contentful.
-
-To create the step, create the file `src/workflows/steps/create-products-contentful.ts` with the following content:
-
-```ts title="src/workflows/steps/create-products-contentful.ts" highlights={createProductsContentfulStepHighlights}
-import { ProductDTO } from "@medusajs/framework/types"
-import { CONTENTFUL_MODULE } from "../../modules/contentful"
-import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
-import ContentfulModuleService from "../../modules/contentful/service"
-import { EntryProps } from "contentful-management"
-
-type StepInput = {
- products: ProductDTO[]
-}
-
-export const createProductsContentfulStep = createStep(
- "create-products-contentful-step",
- async (input: StepInput, { container }) => {
- const contentfulModuleService: ContentfulModuleService =
- container.resolve(CONTENTFUL_MODULE)
-
- const products: EntryProps[] = []
-
- try {
- for (const product of input.products) {
- products.push(await contentfulModuleService.createProduct(product))
- }
- } catch(e) {
- return StepResponse.permanentFailure(
- `Error creating products in Contentful: ${e.message}`,
- products
- )
- }
-
- return new StepResponse(
- products,
- products
- )
- },
- async (products, { container }) => {
- if (!products) {
- return
- }
-
- const contentfulModuleService: ContentfulModuleService =
- container.resolve(CONTENTFUL_MODULE)
-
- for (const product of products) {
- await contentfulModuleService.deleteProduct(product.sys.id)
- }
- }
-)
-```
-
-You create a step with `createStep` from the Workflows SDK. It accepts three parameters:
-
-1. The step's unique name, which is `create-products-contentful-step`.
-2. An async function that receives two parameters:
- - The step's input, which is in this case an object holding an array of products to create in Contentful.
- - 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.
-3. An optional compensation function that undoes the actions performed in the step if an error occurs in the workflow's execution. This mechanism ensures data consistency in your application, especially as you integrate external systems.
-
-The Medusa container is different from the module's container. Since modules are isolated, they each have a container with their resources. Refer to the [Module Container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md) documentation for more information.
-
-In the step function, you resolve the Contentful Module's service from the Medusa container using the name you exported in the module definition's file.
-
-Then, you iterate over the products and create a new entry in Contentful for each product using the `createProduct` method you created earlier. If the creation of any product fails, you fail the step and pass the created products to the compensation function.
-
-A step function must return a `StepResponse` instance. The `StepResponse` constructor accepts two parameters:
-
-1. The step's output, which is the product entries created in Contentful.
-2. Data to pass to the step's compensation function.
-
-The compensation function accepts as a parameter the data passed from the step, and an object containing the Medusa container.
-
-In the compensation function, you iterate over the created product entries and delete them from Contentful using the `deleteProduct` method you created earlier.
-
-#### Create the Workflow
-
-Now that you have all the necessary steps, you can create the workflow.
-
-To create the workflow, create the file `src/workflows/create-products-contentful.ts` with the following content:
-
-```ts title="src/workflows/create-products-contentful.ts"
-import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
-import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
-import { createProductsContentfulStep } from "./steps/create-products-contentful"
-import { ProductDTO } from "@medusajs/framework/types"
-
-type WorkflowInput = {
- product_ids: string[]
-}
-
-export const createProductsContentfulWorkflow = createWorkflow(
- { name: "create-products-contentful-workflow" },
- (input: WorkflowInput) => {
- // @ts-ignore
- const { data } = useQueryGraphStep({
- entity: "product",
- fields: [
- "id",
- "title",
- "description",
- "subtitle",
- "status",
- "handle",
- "variants.*",
- "variants.options.*",
- "options.*",
- "options.values.*",
- ],
- filters: {
- id: input.product_ids,
- },
- })
-
- const contentfulProducts = createProductsContentfulStep({
- products: data as ProductDTO[],
- })
-
- return new WorkflowResponse(contentfulProducts)
- }
-)
-```
-
-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 the product IDs to create in Contentful.
-
-In the workflow's constructor function, you:
-
-1. Retrieve the Medusa products using the `useQueryGraphStep` helper step. This step uses Medusa's [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) tool to retrieve data across modules. You pass it the product IDs to retrieve.
-2. Create the product entries in Contentful using the `createProductsContentfulStep` step.
-
-A workflow must return an instance of `WorkflowResponse`. The `WorkflowResponse` constructor accepts the workflow's output as a parameter, which is an object of the product entries created in Contentful.
-
-You now have the workflow that you can execute when a product is created in Medusa, or when the admin user triggers a sync manually.
-
-***
-
-## Step 4: Trigger Sync on Product Creation
-
-Medusa has an event system that allows you to listen for events, such as `product.created`, and perform an asynchronous action when the event is emitted.
-
-You listen to events in a subscriber. A [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) is an asynchronous function that listens to one or more events and performs actions when these events are emitted. A subscriber is useful when syncing data across systems, as the operation can be time-consuming and should be performed in the background.
-
-In this step, you'll create a subscriber that listens to the `product.created` event and executes the `createProductsContentfulWorkflow` workflow.
-
-Learn more about subscribers in the [Events and Subscribers documentation](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md).
-
-To create a subscriber, create the file `src/subscribers/create-product.ts` with the following content:
-
-```ts title="src/subscribers/create-product.ts" highlights={createProductSubscriberHighlights}
-import {
- type SubscriberConfig,
- type SubscriberArgs,
-} from "@medusajs/framework"
-import {
- createProductsContentfulWorkflow,
-} from "../workflows/create-products-contentful"
-
-export default async function handleProductCreate({
- event: { data },
- container,
-}: SubscriberArgs<{ id: string }>) {
- await createProductsContentfulWorkflow(container)
- .run({
- input: {
- product_ids: [data.id],
- },
- })
-
- console.log("Product created in Contentful")
-}
-
-export const config: SubscriberConfig = {
- event: "product.created",
-}
-```
-
-A subscriber file must export:
-
-1. An asynchronous function, which is the subscriber that is executed when the event is emitted.
-2. A configuration object that holds the name of the event the subscriber listens to, which is `product.created` in this case.
-
-The subscriber function receives an object as a parameter that has the following properties:
-
-- `event`: An object that holds the event's data payload. The payload of the `product.created` event is an array of product IDs.
-- `container`: The Medusa container to access the Framework and commerce tools.
-
-In the subscriber function, you execute the `createProductsContentfulWorkflow` by invoking it, passing the Medusa container as a parameter. Then, you chain a `run` method, passing it the product ID from the event's data payload as input.
-
-Finally, you log a message to the console to indicate that the product was created in Contentful.
-
-### Test the Subscriber
-
-To test out the subscriber, start the Medusa application:
-
-```bash npm2yarn
-npm run dev
-```
-
-Then, open the Medusa Admin dashboard and login.
-
-Can't remember the credentials? Learn how to create a user in the [Medusa CLI reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/medusa-cli/commands/user/index.html.md).
-
-Next, open the Products page and create a new product.
-
-You should see the following message in the terminal:
-
-```bash
-info: Product created in Contentful
-```
-
-You can also see the product in the Contentful dashboard by going to the Content page.
-
-***
-
-## Step 5: Trigger Product Sync Manually
-
-The other way to sync products is when the admin user triggers a sync manually. This is useful when you already have products in Medusa and you want to sync them to Contentful.
-
-To allow admin users to trigger a sync manually, you need:
-
-1. A subscriber that listens to a custom event.
-2. An API route that emits the custom event when a request is sent to it.
-3. A UI route in the Medusa Admin that displays a button to trigger the sync.
-
-### Create Manual Sync Subscriber
-
-You'll start by creating the subscriber that listens to a custom event to sync the Medusa products to Contentful.
-
-To create the subscriber, create the file `src/subscribers/sync-products.ts` with the following content:
-
-```ts title="src/subscribers/sync-products.ts" highlights={syncProductsSubscriberHighlights}
-import type {
- SubscriberConfig,
- SubscriberArgs,
-} from "@medusajs/framework"
-import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
-import {
- createProductsContentfulWorkflow,
-} from "../workflows/create-products-contentful"
-
-export default async function syncProductsHandler({
- container,
-}: SubscriberArgs>) {
- const query = container.resolve(ContainerRegistrationKeys.QUERY)
-
- const batchSize = 100
- let hasMore = true
- let offset = 0
- let totalCount = 0
-
- while (hasMore) {
- const {
- data: products,
- metadata: { count } = {},
- } = await query.graph({
- entity: "product",
- fields: [
- "id",
- ],
- pagination: {
- skip: offset,
- take: batchSize,
- },
- })
-
- if (products.length) {
- await createProductsContentfulWorkflow(container).run({
- input: {
- product_ids: products.map((product) => product.id),
- },
- })
- }
-
- hasMore = products.length === batchSize
- offset += batchSize
- totalCount = count ?? 0
- }
-
- console.log(`Synced ${totalCount} products to Contentful`)
-}
-
-export const config: SubscriberConfig = {
- event: "products.sync",
-}
-```
-
-You create a subscriber that listens to the `products.sync` event.
-
-In the subscriber function, you use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to retrieve all the products in Medusa with pagination. Then, for each batch of products, you execute the `createProductsContentfulWorkflow` workflow, passing the product IDs to the workflow.
-
-Finally, you log a message to the console to indicate that the products were synced to Contentful.
-
-### Create API Route to Trigger Sync
-
-Next, to allow the admin user to trigger the sync manually, you need to create an API route that emits the `products.sync` event.
-
-An API Route is an endpoint that exposes commerce features to external applications and clients, such as storefronts.
-
-Learn more about API routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md).
-
-An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`.
-
-So, to create an API route at the path `/admin/contentful/sync`, create the file `src/api/admin/contentful/sync/route.ts` with the following content:
-
-```ts title="src/api/admin/contentful/sync/route.ts" highlights={syncProductsRouteHighlights}
-import {
- MedusaRequest,
- MedusaResponse,
-} from "@medusajs/framework/http"
-
-export const POST = async (
- req: MedusaRequest,
- res: MedusaResponse
-) => {
- const eventService = req.scope.resolve("event_bus")
-
- await eventService.emit({
- name: "products.sync",
- data: {},
- })
-
- res.status(200).json({
- message: "Products sync triggered successfully",
- })
-}
-```
-
-Since you export a `POST` route handler function, you expose an `API` route at `/admin/contentful/sync`. The route handler function accepts two parameters:
-
-1. A request object with details and context on the request, such as body parameters or authenticated user details.
-2. A response object to manipulate and send the response.
-
-In the route handler, you resolve the [Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/index.html.md)'s service from the Medusa container and emit the `products.sync` event.
-
-### Create UI Route to Trigger Sync
-
-Finally, you'll add a new page to the Medusa Admin dashboard that displays a button to trigger the sync. To add a page, you need to create a UI route.
-
-A [UI route](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md) is a React component that specifies the content to be shown in a new page of the Medusa Admin dashboard. You'll create a UI route to display a button that triggers product syncing to Contentful when clicked.
-
-Refer to the [UI Routes](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md) documentation for more information.
-
-#### Configure JS SDK
-
-Before creating the UI route, you'll configure Medusa's [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md) so that you can use it to send requests to the Medusa server.
-
-The JS SDK is installed by default in your Medusa application. To configure it, create the file `src/admin/lib/sdk.ts` with the following content:
-
-```ts title="src/admin/lib/sdk.ts"
-import Medusa from "@medusajs/js-sdk"
-
-export const sdk = new Medusa({
- baseUrl: "http://localhost:9000",
- debug: process.env.NODE_ENV === "development",
- auth: {
- type: "session",
- },
-})
-```
-
-You create an instance of the JS SDK using the `Medusa` class from the JS SDK. You pass it an object having the following properties:
-
-- `baseUrl`: The base URL of the Medusa server.
-- `debug`: A boolean indicating whether to log debug information into the console.
-- `auth`: An object specifying the authentication type. When using the JS SDK for admin customizations, you use the `session` authentication type.
-
-#### Create UI Route
-
-UI routes are created in a `page.tsx` file under a sub-directory of `src/admin/routes` directory. The file's path relative to `src/admin/routes` determines its path in the dashboard.
-
-So, create the file `src/admin/routes/contentful/page.tsx` with the following content:
-
-```tsx title="src/admin/routes/contentful/page.tsx" highlights={contentfulPageHighlights}
-import { defineRouteConfig } from "@medusajs/admin-sdk"
-import { Container, Heading, Button } from "@medusajs/ui"
-import { useMutation } from "@tanstack/react-query"
-import { sdk } from "../../lib/sdk"
-import { toast } from "@medusajs/ui"
-
-const ContentfulSettingsPage = () => {
- const { mutate, isPending } = useMutation({
- mutationFn: () =>
- sdk.client.fetch("/admin/contentful/sync", {
- method: "POST",
- }),
- onSuccess: () => {
- toast.success("Sync to Contentful triggered successfully")
- },
- })
-
- return (
-
-
-
- Contentful Settings
-
-
-
-
-
-
- )
-}
-
-export const config = defineRouteConfig({
- label: "Contentful",
-})
-
-export default ContentfulSettingsPage
-```
-
-A UI route's file must export:
-
-1. A React component that defines the content of the page.
-2. A configuration object that specifies the route's label in the dashboard. This label is used to show a sidebar item for the new route.
-
-In the React component, you use `useMutation` hook from `@tanstack/react-query` to create a mutation that sends a `POST` request to the API route you created earlier. In the mutation function, you use the JS SDK to send the request.
-
-Then, in the return statement, you display a button that triggers the mutation when clicked, which sends a request to the API route you created earlier.
-
-### Test the Sync
-
-To test out the sync, start the Medusa application:
-
-```bash npm2yarn
-npm run dev
-```
-
-Then, open the Medusa Admin dashboard and login. In the sidebar, you'll find a new "Contentful" item. If you click on it, you'll see the page you created with the button to trigger the sync.
-
-
-
-If you click on the button, you'll see the following message in the terminal:
-
-```bash
-info: Synced 4 products to Contentful
-```
-
-Assuming you have `4` products in Medusa, the message indicates that the sync was successful.
-
-You can also see the products in the Contentful dashboard.
-
-
-
-***
-
-## Step 6: Retrieve Locales API Route
-
-In the next steps, you'll implement customizations that are useful for storefronts. A storefront should show the customer a list of available locales and allow them to select from them.
-
-In this step, you will:
-
-1. Add the logic to retrieve locales from Contentful in the Contentful Module's service.
-2. Create an API route that exposes the locales to the storefront.
-3. Customize the Next.js Starter Storefront to show the locales to customers.
-
-### Retrieve Locales from Contentful Method
-
-You'll start by adding two methods to the Contentful Module's service that are useful to retrieve locales from Contentful.
-
-The first method retrieves all locales from Contentful. Add it to the service at `src/modules/contentful/service.ts`:
-
-```ts title="src/modules/contentful/service.ts"
-export default class ContentfulModuleService {
- // ...
- async getLocales() {
- return await this.managementClient.locale.getMany({})
- }
-}
-```
-
-You use the `locale.getMany` method of the Contentful Management API client to retrieve all locales.
-
-The second method returns the code of the default locale:
-
-```ts title="src/modules/contentful/service.ts"
-export default class ContentfulModuleService {
- // ...
- async getDefaultLocaleCode() {
- return this.options.default_locale
- }
-}
-```
-
-You return the default locale using the `default_locale` option you set in the module's options.
-
-### Create API Route to Retrieve Locales
-
-Next, you'll create an API route that exposes the locales to the storefront.
-
-To create the API route, create the file `src/api/store/locales/route.ts` with the following content:
-
-```ts title="src/api/store/locales/route.ts" highlights={getLocalesRouteHighlights}
-import {
- MedusaRequest,
- MedusaResponse,
-} from "@medusajs/framework/http"
-import { CONTENTFUL_MODULE } from "../../../modules/contentful"
-import ContentfulModuleService from "../../../modules/contentful/service"
-
-export const GET = async (
- req: MedusaRequest,
- res: MedusaResponse
-) => {
- const contentfulModuleService: ContentfulModuleService = req.scope.resolve(
- CONTENTFUL_MODULE
- )
-
- const locales = await contentfulModuleService.getLocales()
- const defaultLocaleCode = await contentfulModuleService.getDefaultLocaleCode()
-
- const formattedLocales = locales.items.map((locale) => {
- return {
- name: locale.name,
- code: locale.code,
- is_default: locale.code === defaultLocaleCode,
- }
- })
-
- res.json({
- locales: formattedLocales,
- })
-}
-```
-
-Since you export a `GET` route handler function, you expose a `GET` route at `/store/locales`.
-
-In the route handler, you resolve the Contentful Module's service from the Medusa container to retrieve the locales and the default locale code.
-
-Then, you format the locales to include their name, code, and whether they are the default locale.
-
-Finally, you return the formatted locales in the JSON response.
-
-### Customize Storefront to Show Locales
-
-In the first step of this tutorial, you installed the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) along with the Medusa application. This storefront provides ecommerce features like a product catalog, a cart, and a checkout.
-
-In this section, you'll customize the storefront to show the locales to customers and allow them to select from them. The selected locale will be stored in the browser's cookies, allowing you to use it later when retrieving a product's localized data.
-
-The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.
-
-So, if your Medusa application's directory is `medusa-contentful`, you can find the storefront by going back to the parent directory and changing to the `medusa-contentful-storefront` directory:
-
-```bash
-cd ../medusa-contentful-storefront # change based on your project name
-```
-
-#### Add Cookie Functions
-
-You'll start by adding two functions that retrieve and set the locale in the browser's cookies.
-
-In `src/lib/data/cookies.ts` add the following functions:
-
-```ts title="src/lib/data/cookies.ts" highlights={getLocaleHighlights} badgeLabel="Storefront" badgeColor="blue"
-export const getLocale = async () => {
- const cookies = await nextCookies()
- return cookies.get("_medusa_locale")?.value
-}
-
-export const setLocale = async (locale: string) => {
- const cookies = await nextCookies()
- cookies.set("_medusa_locale", locale, {
- maxAge: 60 * 60 * 24 * 7,
- })
-}
-```
-
-The `getLocale` function retrieves the locale from the browser's cookies, and the `setLocale` function sets the locale in the browser's cookies.
-
-#### Manage Locales Functions
-
-Next, you'll add server actions to retrieve the locales and set the selected locale.
-
-Create the file `src/lib/data/locale.ts` with the following content:
-
-```ts title="src/lib/data/locale.ts" highlights={getLocalesHighlights} badgeLabel="Storefront" badgeColor="blue"
-"use server"
-
-import { sdk } from "@lib/config"
-import type { Document } from "@contentful/rich-text-types"
-import { getLocale, setLocale } from "./cookies"
-
-export type Locale = {
- name: string
- code: string
- is_default: boolean
-}
-
-export async function getLocales() {
- return await sdk.client.fetch<{
- locales: Locale[]
- }>("/store/locales")
-}
-
-export async function getSelectedLocale() {
- let localeCode = await getLocale()
- if (!localeCode) {
- const locales = await getLocales()
- localeCode = locales.locales.find((l) => l.is_default)?.code
- }
- return localeCode
-}
-
-export async function setSelectedLocale(locale: string) {
- await setLocale(locale)
-}
-```
-
-You add the following functions:
-
-1. `getLocales`: Retrieves the locales from the Medusa server using the API route you created earlier.
-2. `getSelectedLocale`: Retrieves the selected locale from the browser's cookies, or the default locale if no locale is selected.
-3. `setSelectedLocale`: Sets the selected locale in the browser's cookies.
-
-You'll use these functions as you add the UI to show the locales next.
-
-#### Show Locales in the Storefront
-
-You'll now add the UI to show the locales to customers and allow them to select from them.
-
-Create the file `src/modules/layout/components/locale-select/index.tsx` with the following content:
-
-```tsx title="src/modules/layout/components/locale-select/index.tsx" highlights={localeSelectHighlights} badgeLabel="Storefront" badgeColor="blue"
-"use client"
-
-import { useState, useEffect, Fragment } from "react"
-import { getLocales, Locale, getSelectedLocale, setSelectedLocale } from "../../../../lib/data/locale"
-import { Listbox, ListboxButton, ListboxOption, ListboxOptions, Transition } from "@headlessui/react"
-import { ArrowRightMini } from "@medusajs/icons"
-import { clx } from "@medusajs/ui"
-
-const LocaleSelect = () => {
- const [locales, setLocales] = useState([])
- const [locale, setLocale] = useState()
- const [open, setOpen] = useState(false)
-
- useEffect(() => {
- getLocales()
- .then(({ locales }) => {
- setLocales(locales)
- })
- }, [])
-
- useEffect(() => {
- if (!locales.length || locale) {
- return
- }
-
- getSelectedLocale().then((locale) => {
- const localeDetails = locales.find((l) => l.code === locale)
- setLocale(localeDetails)
- })
- }, [locales])
-
- useEffect(() => {
- if (locale) {
- setSelectedLocale(locale.code)
- }
- }, [locale])
-
- const handleChange = (locale: Locale) => {
- setLocale(locale)
- setOpen(false)
- }
-
- // TODO add return statement
-}
-
-export default LocaleSelect
-```
-
-You create a `LocaleSelect` component with the following state variables:
-
-1. `locales`: The list of locales retrieved from the Medusa server.
-2. `locale`: The selected locale.
-3. `open`: A boolean indicating whether the dropdown is open.
-
-Then, you use three `useEffect` hooks:
-
-1. The first `useEffect` hook retrieves the locales using the `getLocales` function and sets them in the `locales` state variable.
-2. The second `useEffect` hook is triggered when the `locales` state variable changes. It retrieves the selected locale using the `getSelectedLocale` function and sets the `locale` state variable.
-3. The third `useEffect` hook is triggered when the `locale` state variable changes. It sets the selected locale in the browser's cookies using the `setSelectedLocale` function.
-
-You also create a `handleChange` function that sets the selected locale and closes the dropdown. You'll execute this function when the customer selects a locale from the dropdown.
-
-Finally, you'll add a return statement that shows the locale dropdown. Replace the `TODO` with the following:
-
-```tsx title="src/modules/layout/components/locale-select/index.tsx" badgeLabel="Storefront" badgeColor="blue"
-return (
-
-)
-```
-
-You show the selected locale. Then, when the customer hovers over the locale, the dropdown is shown to select a different locale.
-
-When the customer selects a locale, you execute the `handleChange` function, which sets the selected locale and closes the dropdown.
-
-#### Add Locale Select to the Side Menu
-
-The last step is to show the locale selector in the side menu after the country selector.
-
-In `src/modules/layout/components/side-menu/index.tsx`, add the following import:
-
-```tsx title="src/modules/layout/components/side-menu/index.tsx" badgeLabel="Storefront" badgeColor="blue"
-import LocaleSelect from "../locale-select"
-```
-
-Then, add the `LocaleSelect` component in the return statement of the `SideMenu` component, after the `div` wrapping the country selector:
-
-```tsx title="src/modules/layout/components/side-menu/index.tsx" badgeLabel="Storefront" badgeColor="blue"
-
-```
-
-The locale selector will now show in the side menu after the country selector.
-
-### Test out the Locale Selector
-
-To test out all the changes made in this step, start the Medusa application by running the following command in the Medusa application's directory:
-
-```bash npm2yarn
-npm run dev
-```
-
-Then, start the Next.js Starter Storefront by running the following command in the storefront's directory:
-
-```bash npm2yarn
-npm run dev
-```
-
-The storefront will run at `http://localhost:8000`. Open it in your browser, then click on "Menu" at the top right. You'll see at the bottom of the side menu the locale selector.
-
-
-
-You can try selecting a different locale. The selected locale will be stored, but products will still be shown in the default locale. You'll implement the locale-based product retrieval in the next step.
-
-***
-
-## Step 7: Retrieve Product Details for Locale
-
-The next feature you'll implement is retrieving and displaying product details for a selected locale.
-
-You'll implement this feature by:
-
-1. Linking Medusa's product to Contentful's product.
-2. Adding the method to retrieve product details for a selected locale from Contentful.
-3. Adding a new route to retrieve the product details for a selected locale.
-4. Customizing the storefront to show the product details for the selected locale.
-
-### Link Medusa's Product to Contentful's Product
-
-Medusa facilitates retrieving data across systems using [module links](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index.html.md). A module link forms an association between data models of two modules while maintaining module isolation.
-
-Not only do module links support Medusa data models, but they also support virtual data models that are not persisted in Medusa's database. In that case, you create a [read-only module link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/read-only/index.html.md) that allows you to retrieve data across systems.
-
-In this section, you'll define a read-only module link between Medusa's product and Contentful's product, allowing you to later retrieve a product's entry in Contentful within a single query.
-
-Learn more about read-only module links in the [Read-Only Module Links](https://docs.medusajs.com/docs/learn/fundamentals/module-links/read-only/index.html.md) documentation.
-
-Module links are defined in a TypeScript or JavaScript file under the `src/links` directory. So, create the file `src/links/product-contentful.ts` with the following content:
-
-```ts title="src/links/product-contentful.ts" highlights={productContentfulLinkHighlights}
-import { defineLink } from "@medusajs/framework/utils"
-import ProductModule from "@medusajs/medusa/product"
-import { CONTENTFUL_MODULE } from "../modules/contentful"
-
-export default defineLink(
- {
- linkable: ProductModule.linkable.product,
- field: "id",
- },
- {
- linkable: {
- serviceName: CONTENTFUL_MODULE,
- alias: "contentful_product",
- primaryKey: "product_id",
- },
- },
- {
- readOnly: true,
- }
-)
-```
-
-You define a module link using `defineLink` from the Modules SDK. It accepts three parameters:
-
-1. An object with the linkable configuration of the data model in Medusa, and the field that will be passed as a filter to the Contentful Module's service.
-2. An object with the linkable configuration of the virtual data model in Contentful. This object must have the following properties:
- - `serviceName`: The name of the service, which is the Contentful Module's name. Medusa uses this name to resolve the module's service from the Medusa container.
- - `alias`: The alias to use when querying the linked records. You'll see how that works in a bit.
- - `primaryKey`: The field in Contentful's virtual data model that holds the ID of a product.
-3. An object with the `readOnly` property set to `true`.
-
-You'll see how the module link works in the upcoming steps.
-
-### List Contentful Products Method
-
-Next, you'll add a method that lists Contentful products for a given locale.
-
-Add the following method to the Contentful Module's service at `src/modules/contentful/service.ts`:
-
-```ts title="src/modules/contentful/service.ts" highlights={listContentfulProductsMethodHighlights}
-export default class ContentfulModuleService {
- // ...
- async list(
- filter: {
- id: string | string[]
- context?: {
- locale: string
- }
- }
- ) {
- const contentfulProducts = await this.deliveryClient.getEntries({
- limit: 15,
- content_type: "product",
- "fields.medusaId": filter.id,
- locale: filter.context?.locale,
- include: 3,
- })
-
- return contentfulProducts.items.map((product) => {
- // remove links
- const { productVariants: _, productOptions: __, ...productFields } = product.fields
- return {
- ...productFields,
- product_id: product.fields.medusaId,
- variants: product.fields.productVariants.map((variant) => {
- // remove circular reference
- const { product: _, productOptionValues: __, ...variantFields } = variant.fields
- return {
- ...variantFields,
- product_variant_id: variant.fields.medusaId,
- options: variant.fields.productOptionValues.map((option) => {
- // remove circular reference
- const { productOption: _, ...optionFields } = option.fields
- return {
- ...optionFields,
- product_option_id: option.fields.medusaId,
- }
- }),
- }
- }),
- options: product.fields.productOptions.map((option) => {
- // remove circular reference
- const { product: _, ...optionFields } = option.fields
- return {
- ...optionFields,
- product_option_id: option.fields.medusaId,
- values: option.fields.values.map((value) => {
- // remove circular reference
- const { productOptionValue: _, ...valueFields } = value.fields
- return {
- ...valueFields,
- product_option_value_id: value.fields.medusaId,
- }
- }),
- }
- }),
- }
- })
- }
-}
-```
-
-You add a `list` method that accepts an object with the following properties:
-
-1. `id`: The ID of the product(s) in Medusa to retrieve their entries in Contentful.
-2. `context`: An object with the `locale` property that holds the locale code to retrieve the product's entry in Contentful for that locale.
-
-In the method, you use the Delivery API client's `getEntries` method to retrieve the products. You pass the following parameters:
-
-- `limit`: The maximum number of products to retrieve.
-- `content_type`: The content type of the entries to retrieve, which is `product`.
-- `fields.medusaId`: Filter the products by their `medusaId` field, which holds the ID of the product in Medusa.
-- `locale`: The locale code to retrieve the fields of the product in that locale.
-- `include`: The depth of the included nested entries. This ensures that you can retrieve the product's variants and options, and their values.
-
-Then, you format the retrieved products to:
-
-- Pass the product's ID in the `product_id` property. This is essential to map a product in Medusa to its entry in Contentful.
-- Remove the circular references in the product's variants, options, and values to avoid infinite loops.
-
-To paginate the retrieved products, implemet a `listAndCount` method as explained in the [Query Context](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query-context#using-pagination-with-query/index.html.md) documentation.
-
-### Retrieve Product Details for Locale API Route
-
-You'll now create the API route that returns a product's details for a given locale.
-
-You can create an API route that accepts path parameters by creating a directory within the route file's path whose name is of the format `[param]`.
-
-So, create the file `src/api/store/products/[id]/[locale]/route.ts` with the following content:
-
-```ts title="src/api/store/products/[id]/[locale]/route.ts" highlights={getProductLocaleDetailsRouteHighlights}
-import {
- MedusaRequest,
- MedusaResponse,
-} from "@medusajs/framework/http"
-import { QueryContext } from "@medusajs/framework/utils"
-
-export const GET = async (
- req: MedusaRequest,
- res: MedusaResponse
-) => {
- const { locale, id } = req.params
-
- const query = req.scope.resolve("query")
-
- const { data } = await query.graph({
- entity: "product",
- fields: [
- "id",
- "contentful_product.*",
- ],
- filters: {
- id,
- },
- context: {
- contentful_product: QueryContext({
- locale,
- }),
- },
- })
-
- res.json({
- product: data[0],
- })
-}
-```
-
-Since you export a `GET` route handler function, you expose a `GET` route at `/store/products/[id]/[locale]`. The route accepts two path parameters: the product's ID and the locale code.
-
-In the route handler, you retrieve the `locale` and `id` path parameters from the request. Then, you resolve [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) from the Medusa container.
-
-Next, you use Query to retrieve the localized details of the specified product. To do that, you pass an object with the following properties:
-
-- `entity`: The entity to retrieve, which is `product`.
-- `fields`: The fields to retrieve. Notice that you include the `contentful_product.*` field, which is available through the module link you created earlier.
-- `filters`: The filter to apply on the retrieved products. You apply the product's ID as a filter.
-- `context`: An additional context to be passed to the methods retrieving the data. To pass a context, you use [Query Context](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query-context/index.html.md).
-
-By specifying `contentful_product.*` in the `fields` property, Medusa will retrieve the product's entry from Contentful using the `list` method you added to the Contentful Module's service.
-
-Medusa passes the filters and context to the `list` method, and attaches the returned data to the Medusa product if its `product_id` matches the product's ID.
-
-Finally, you return the product's details in the JSON response.
-
-You can now use this route to retrieve a product's details for a given locale.
-
-### Show Localized Product Details in Storefront
-
-Now that you expose the localized product details, you can customize the storefront to show them.
-
-#### Install Contentful Rich Text Package
-
-When you retrieve the entries from Contentful, rich-text fields are returned as an object that requires special rendering. So, Contentful provides a package to render rich-text fields.
-
-Install the package by running the following command:
-
-```bash npm2yarn
-npm install @contentful/@contentful/rich-text-types
-```
-
-You'll use this package to render the product's description.
-
-#### Retrieve Localized Product Details Function
-
-To retrieve a product's details for a given locale, you'll add a function that sends a request to the API route you created.
-
-First, add the following import at the top of `src/lib/data/locale.ts`:
-
-```ts title="src/lib/data/locale.ts" badgeLabel="Storefront" badgeColor="blue"
-import type { Document } from "@contentful/rich-text-types"
-```
-
-Then, add the following type and function at the end of the file:
-
-```ts title="src/lib/data/locale.ts" badgeLabel="Storefront" badgeColor="blue"
-export type ProductLocaleDetails = {
- id: string
- contentful_product: {
- product_id: string
- title: string
- handle: string
- description: Document
- subtitle?: string
- variants: {
- title: string
- product_variant_id: string
- options: {
- value: string
- product_option_id: string
- }[]
- }[]
- options: {
- title: string
- product_option_id: string
- values: {
- title: string
- product_option_value_id: string
- }[]
- }[]
- }
-}
-
-export async function getProductLocaleDetails(
- productId: string
-) {
- const localeCode = await getSelectedLocale()
-
- return await sdk.client.fetch<{
- product: ProductLocaleDetails
- }>(`/store/products/${productId}/${localeCode}`)
-}
-```
-
-You define a `ProductLocaleDetails` type that describes the structure of a localized product's details.
-
-You also define a `getProductLocaleDetails` function that sends a request to the API route you created and returns the localized product's details.
-
-#### Show Localized Product Title in Products Listing
-
-Next, you'll customize existing components to show the localized product details.
-
-The component defined in `src/modules/products/components/product-preview/index.tsx` shows the product's details in the products listing page. You need to retrieve the localized product details and show the product's title in the selected locale.
-
-In `src/modules/products/components/product-preview/index.tsx`, add the following import:
-
-```tsx title="src/modules/products/components/product-preview/index.tsx" badgeLabel="Storefront" badgeColor="blue"
-import { getProductLocaleDetails } from "@lib/data/locale"
-```
-
-Then, in the `ProductPreview` component in the same file, add the following before the `return` statement:
-
-```tsx title="src/modules/products/components/product-preview/index.tsx" badgeLabel="Storefront" badgeColor="blue"
-const productLocaleDetails = await getProductLocaleDetails(product.id!)
-```
-
-This will retrieve the localized product details for the selected locale.
-
-Finally, to show the localized product title, find in the `ProductPreview` component's `return` statement the following line:
-
-```tsx title="src/modules/products/components/product-preview/index.tsx" badgeLabel="Storefront" badgeColor="blue"
-{product.title}
-```
-
-And replace it with the following:
-
-```tsx title="src/modules/products/components/product-preview/index.tsx" badgeLabel="Storefront" badgeColor="blue"
-{productLocaleDetails.product.contentful_product?.title || product.title}
-```
-
-You'll test it out after the next step.
-
-#### Show Localized Product Details in Product Page
-
-Next, you'll customize the product page to show the localized product details.
-
-The product's details page is defined in `src/app/[countryCode]/(main)/products/[handle]/page.tsx`. So, add the following import at the top of the file:
-
-```tsx title="src/app/[countryCode]/(main)/products/[handle]/page.tsx" badgeLabel="Storefront" badgeColor="blue"
-import { getProductLocaleDetails } from "@lib/data/locale"
-```
-
-Then, in the `ProductPage` component in the same file, add the following before the `return` statement:
-
-```tsx title="src/app/[countryCode]/(main)/products/[handle]/page.tsx" badgeLabel="Storefront" badgeColor="blue"
-const productLocaleDetails = await getProductLocaleDetails(pricedProduct.id!)
-```
-
-This will retrieve the localized product details for the selected locale.
-
-Finally, in the `ProductPage` component in the same file, pass the following prop to `ProductTemplate`:
-
-```tsx title="src/app/[countryCode]/(main)/products/[handle]/page.tsx" badgeLabel="Storefront" badgeColor="blue"
-return (
-
-)
-```
-
-Next, you'll customize the `ProductTemplate` component to accept and use this prop.
-
-In `src/modules/products/templates/index.tsx`, add the following import:
-
-```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue"
-import { ProductLocaleDetails } from "@lib/data/locale"
-```
-
-Then, update the `ProductTemplateProps` type to include the `productLocaleDetails` prop:
-
-```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue"
-export type ProductTemplateProps = {
- // ...
- productLocaleDetails: ProductLocaleDetails
-}
-```
-
-Next, update the `ProductTemplate` component to destructure the `productLocaleDetails` prop:
-
-```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue"
-const ProductTemplate: React.FC = ({
- // ...
- productLocaleDetails,
-}) => {
- // ...
-}
-```
-
-Finally, pass the `productLocaleDetails` prop to the `ProductInfo` component in the `return` statement:
-
-```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue"
-
-```
-
-The `ProductInfo` component shows the product's details. So, you need to update it to accept and use the `productLocaleDetails` prop.
-
-In `src/modules/products/templates/product-info/index.tsx`, add the following imports:
-
-```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue"
-import { ProductLocaleDetails } from "@lib/data/locale"
-import { documentToHtmlString } from "@contentful/rich-text-html-renderer"
-```
-
-Then, update the `ProductInfoProps` type to include the `productLocaleDetails` prop:
-
-```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue"
-export type ProductInfoProps = {
- // ...
- productLocaleDetails: ProductLocaleDetails
-}
-```
-
-Next, update the `ProductInfo` component to destructure the `productLocaleDetails` prop:
-
-```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue"
-const ProductInfo = ({ product, productLocaleDetails }: ProductInfoProps) => {
- // ...
-}
-```
-
-Then, find the following line in the `return` statement:
-
-```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue"
-{product.title}
-```
-
-And replace it with the following:
-
-```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue"
-{productLocaleDetails.contentful_product?.title || product.title}
-```
-
-Also, find the following line:
-
-```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue"
-{product.description}
-```
-
-And replace it with the following:
-
-```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue"
-{productLocaleDetails.contentful_product?.description ?
- :
- product.description
-}
-```
-
-You use the `documentToHtmlString` function to render the rich-text field. The function returns an HTML string that you can use to render the description.
-
-### Test out the Localized Product Details
-
-You can now test out all the changes made in this step.
-
-To do that, start the Medusa application by running the following command in the Medusa application's directory:
-
-```bash npm2yarn
-npm run dev
-```
-
-Then, start the Next.js Starter Storefront by running the following command in the storefront's directory:
-
-```bash npm2yarn
-npm run dev
-```
-
-Open the storefront at `http://localhost:8000` and select a different locale.
-
-Then, open the products listing page by clicking on Menu -> Store. You'll see the product titles in the selected locale.
-
-
-
-Then, if you click on a product, you'll see the product's title and description in the selected locale.
-
-
-
-***
-
-## Step 8: Sync Changes from Contentful to Medusa
-
-The last feature you'll implement is syncing changes from Contentful to Medusa.
-
-Contentful's webhooks allow you to listen to changes in your Contentful entries. You can then set up a webhook listener API route in Medusa that updates the product's data.
-
-In this step, you'll set up a webhook listener that updates Medusa's product data when a Contentful entry is published.
-
-### Prerequisites: Public Server
-
-Webhooks can only trigger deployed listeners. So, you must either [deploy your Medusa application](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/deployment/index.html.md), or use tools like [ngrok](https://ngrok.com/) to publicly expose your local application.
-
-### Set Up Webhooks in Contentful
-
-Before setting up the webhook listener, you need to set up a webhook in Contentful. To do that, on the Contentful dashboard:
-
-1. Click on the cog icon at the top right, then choose "Webhooks" from the dropdown.
-
-
-
-2. On the Webhooks page, click on the "Add Webhook" button.
-3. In the webhook form:
- - In the Name fields, enter a name, such as "Medusa".
- - In the URL field, enter `{your_app}/hooks/contentful`, where `{your_app}` is the public URL of your Medusa application. You'll create the `/hooks/contentful` API route in a bit.
- - In the Triggers section, select the "Published" trigger for "Entry".
-
-
-
-- Scroll down to the "Headers" section, and choose "application/json" for the "Content type" field.
-
-
-
-4. Once you're done, click the Save button.
-
-### Setup Webhook Secret in Contentful
-
-You also need to add a webhook secret in Contentful. To do that, on the Contentful dashboard:
-
-1. Click on the cog icon at the top right, then choose "Webhooks" from the dropdown.
-2. On the Webhooks page, click on the "Settings" tab.
-3. Click on the "Enable request verification" button.
-
-
-
-4. Copy the secret that shows up. You can update it later but you can't see the same secret again.
-
-You'll use the secret to verify the webhook request in Medusa next.
-
-### Update Contentful Module Options
-
-First, add the webhook secret as an environment variable in the Medusa application's `.env` file:
-
-```plain
-CONTENTFUL_WEBHOOK_SECRET=aEl7...
-```
-
-Next, add the webhook secret to the Contentful Module options in the Medusa application's `medusa-config.ts` file:
-
-```ts title="medusa-config.ts"
-module.exports = defineConfig({
- // ...
- modules: [
- {
- resolve: "./src/modules/contentful",
- options: {
- // ...
- webhook_secret: process.env.CONTENTFUL_WEBHOOK_SECRET,
- },
- },
- ],
-})
-```
-
-Finally, update the `ModuleOptions` type in `src/modules/contentful/loader/create-content-models.ts` to include the `webhook_secret` option:
-
-```ts title="src/modules/contentful/loader/create-content-models.ts"
-export type ModuleOptions = {
- // ...
- webhook_secret: string
-}
-```
-
-### Add Verify Request Method
-
-Next, you'll add a method to the Contentful Module's service that verifies a webhook request.
-
-To verify the request, you'll need the `@contentful/node-apps-toolkit` package that provides utility functions for Node.js applications.
-
-So, run the following command to install it:
-
-```bash npm2yarn
-npm install @contentful/node-apps-toolkit
-```
-
-Then, add the following method to the Contentful Module's service in `src/modules/contentful/service.ts`:
-
-```ts title="src/modules/contentful/service.ts"
-// other imports...
-import {
- CanonicalRequest,
- verifyRequest,
-} from "@contentful/node-apps-toolkit"
-
-export default class ContentfulModuleService {
- // ...
- async verifyWebhook(request: CanonicalRequest) {
- if (!this.options.webhook_secret) {
- throw new MedusaError(
- MedusaError.Types.INVALID_DATA,
- "Webhook secret is not set"
- )
- }
- return verifyRequest(this.options.webhook_secret, request, 0)
- }
-}
-```
-
-You add a `verifyWebhook` method that verifies a webhook request using the `verifyRequest` function.
-
-You pass to the `verifyRequest` function the webhook secret from the module's options with the request's details. You also disable time-to-live (TTL) check by passing `0` as the third argument.
-
-### Handle Contentful Webhook Workflow
-
-Before you add the webhook listener, the last piece you need is to add a workflow that handles the necessary updates based on the webhook event.
-
-The workflow will have the following steps:
-
-- [prepareUpdateDataStep](#prepareUpdateDataStep): Prepare the data for the update
-
-You only need to implement the first step, as Medusa provides the other workflows (running as steps) in the `@medusajs/medusa/core-flows` package.
-
-#### prepareUpdateDataStep
-
-The first step receives the webhook data payload and, based on the entry type, returns the data necessary for the update.
-
-To create the step, create the file `src/workflows/steps/prepare-update-data.ts` with the following content:
-
-```ts title="src/workflows/steps/prepare-update-data.ts" highlights={prepareUpdateDataStepHighlights}
-import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
-import { EntryProps } from "contentful-management"
-import ContentfulModuleService from "../../modules/contentful/service"
-import { CONTENTFUL_MODULE } from "../../modules/contentful"
-
-type StepInput = {
- entry: EntryProps
-}
-
-export const prepareUpdateDataStep = createStep(
- "prepare-update-data",
- async ({ entry }: StepInput, { container }) => {
- const contentfulModuleService: ContentfulModuleService =
- container.resolve(CONTENTFUL_MODULE)
-
- const defaultLocale = await contentfulModuleService.getDefaultLocaleCode()
-
- let data: Record = {}
-
- switch (entry.sys.contentType.sys.id) {
- case "product":
- data = {
- id: entry.fields.medusaId[defaultLocale!],
- title: entry.fields.title[defaultLocale!],
- subtitle: entry.fields.subtitle?.[defaultLocale!] || undefined,
- handle: entry.fields.handle[defaultLocale!],
- }
- break
- case "productVariant":
- data = {
- id: entry.fields.medusaId[defaultLocale!],
- title: entry.fields.title[defaultLocale!],
- }
- break
- case "productOption":
- data = {
- selector: {
- id: entry.fields.medusaId[defaultLocale!],
- },
- update: {
- title: entry.fields.title[defaultLocale!],
- },
- }
- break
- }
-
- return new StepResponse(data)
- }
-)
-```
-
-You define a `prepareUpdateDataStep` function that receives the webhook data payload as an input.
-
-In the step, you resolve the Contentful Module's service and use it to retrieve the default locale code. You need it to find the value to update the fields in Medusa.
-
-Next, you prepare the data to return based on the entry type:
-
-- `product`: The product's ID, title, subtitle, and handle.
-- `productVariant`: The product variant's ID and title.
-- `productOption`: The product option's ID and title.
-
-The data is prepared based on the expected input for the workflows that will be used to update the data.
-
-#### Create the Workflow
-
-You can now create the workflow that handles the webhook event.
-
-Create the file `src/workflows/handle-contentful-hook.ts` with the following content:
-
-```ts title="src/workflows/handle-contentful-hook.ts" highlights={handleContentfulHookWorkflowHighlights} collapsibleLines="1-14" expandButtonLabel="Show Imports"
-import { createWorkflow, when } from "@medusajs/framework/workflows-sdk"
-import { EntryProps } from "contentful-management"
-import { prepareUpdateDataStep } from "./steps/prepare-update-data"
-import {
- updateProductOptionsWorkflow,
- updateProductsWorkflow,
- updateProductVariantsWorkflow,
- UpdateProductOptionsWorkflowInput,
-} from "@medusajs/medusa/core-flows"
-import {
- UpsertProductDTO,
- UpsertProductVariantDTO,
-} from "@medusajs/framework/types"
-
-export type WorkflowInput = {
- entry: EntryProps
-}
-
-export const handleContentfulHookWorkflow = createWorkflow(
- { name: "handle-contentful-hook-workflow" },
- (input: WorkflowInput) => {
- const prepareUpdateData = prepareUpdateDataStep({
- entry: input.entry,
- })
-
- when(input, (input) => input.entry.sys.contentType.sys.id === "product")
- .then(() => {
- updateProductsWorkflow.runAsStep({
- input: {
- products: [prepareUpdateData as UpsertProductDTO],
- },
- })
- })
-
- when(input, (input) =>
- input.entry.sys.contentType.sys.id === "productVariant"
- )
- .then(() => {
- updateProductVariantsWorkflow.runAsStep({
- input: {
- product_variants: [prepareUpdateData as UpsertProductVariantDTO],
- },
- })
- })
-
- when(input, (input) =>
- input.entry.sys.contentType.sys.id === "productOption"
- )
- .then(() => {
- updateProductOptionsWorkflow.runAsStep({
- input: prepareUpdateData as unknown as UpdateProductOptionsWorkflowInput,
- })
- })
- }
-)
-```
-
-You define a `handleContentfulHookWorkflow` function that receives the webhook data payload as an input.
-
-In the workflow, you:
-
-- Prepare the data for the update using the `prepareUpdateDataStep` step.
-- Use a [when](https://docs.medusajs.com/docs/learn/fundamentals/workflows/conditions/index.html.md) condition to check if the entry type is a `product`, and if so, update the product using the `updateProductsWorkflow`.
-- Use a `when` condition to check if the entry type is a `productVariant`, and if so, update the product variant using the `updateProductVariantsWorkflow`.
-- Use a `when` condition to check if the entry type is a `productOption`, and if so, update the product option using the `updateProductOptionsWorkflow`.
-
-You can't perform data manipulation in a workflow's constructor function. Instead, the Workflows SDK includes utility functions like `when` to perform typical operations that requires accessing data values. Learn more about workflow constraints in the [Workflow Constraints](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md) documetation.
-
-### Add the Webhook Listener API Route
-
-You can finally add the API route that acts as a webhook listener.
-
-To add the API route, create the file `src/api/hooks/contentful/route.ts` with the following content:
-
-```ts title="src/api/hooks/contentful/route.ts" highlights={contentfulHookRouteHighlights} collapsibleLines="1-10" expandButtonLabel="Show Imports"
-import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
-import {
- handleContentfulHookWorkflow,
- HandleContentfulHookWorkflowInput,
-} from "../../../workflows/handle-contentful-hook"
-import { CONTENTFUL_MODULE } from "../../../modules/contentful"
-import { CanonicalRequest } from "@contentful/node-apps-toolkit"
-import { MedusaError } from "@medusajs/framework/utils"
-import ContentfulModuleService from "../../../modules/contentful/service"
-
-export const POST = async (
- req: MedusaRequest,
- res: MedusaResponse
-) => {
- const contentfulModuleService: ContentfulModuleService =
- req.scope.resolve(CONTENTFUL_MODULE)
-
- const isValid = await contentfulModuleService.verifyWebhook({
- path: req.path,
- method: req.method.toUpperCase(),
- headers: req.headers,
- body: JSON.stringify(req.body),
- } as unknown as CanonicalRequest)
-
- if (!isValid) {
- throw new MedusaError(
- MedusaError.Types.UNAUTHORIZED,
- "Invalid webhook request"
- )
- }
-
- await handleContentfulHookWorkflow(req.scope).run({
- input: {
- entry: req.body,
- } as unknown as HandleContentfulHookWorkflowInput,
- })
-
- res.status(200).send("OK")
-}
-```
-
-Since you export a `POST` route handler function, you expose a `POST` route at `/hooks/contentful`.
-
-In the route, you first use the `verifyWebhook` method of the Contentful Module's service to verify the request. If the request is invalid, you throw an error.
-
-Then, you run the `handleContentfulHookWorkflow` passing the request's body, which is the webhook data payload, as an input.
-
-Finally, you return a `200` response to Contentful to confirm that the webhook was received and processed.
-
-### Test the Webhook Listener
-
-To test out the webhook listener, start the Medusa application:
-
-```bash npm2yarn
-npm run dev
-```
-
-Then, try updating a product's title (in the default locale) in Contentful. You should see the product's title updated in Medusa.
-
-***
-
-## Next Steps
-
-You've now integrated Contentful with Medusa and supported localized product details. You can expand on the features in this tutorial to:
-
-1. Add support for other data types, such as product categories or collections.
- - Refer to the data model references for each [Commerce Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md) to figure out the content types you need to create in Contentful.
-2. Listen to other product events and update the Contentful entries accordingly.
- - Refer to the [Events Reference](https://docs.medusajs.com/references/events/index.html.md) for details on all events emitted in Medusa.
-3. Add localization for the entire Next.js Starter Storefront. You can either:
- - Create content types in Contentful for different sections in the storefront, then use them to retrieve the localized content;
- - Or use the approaches recommended in the [Next.js documentation](https://nextjs.org/docs/app/building-your-application/routing/internationalization).
-
-If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth learning of all the concepts you've used in this guide and more.
-
-To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).
-
-### Troubleshooting
-
-If you encounter issues during your development, check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/index.html.md).
-
-### Getting Help
-
-If you encounter issues not covered in the troubleshooting guides:
-
-1. Visit the [Medusa GitHub repository](https://github.com/medusajs/medusa) to report issues or ask questions.
-2. Join the [Medusa Discord community](https://discord.gg/medusajs) for real-time support from community members.
-3. Contact the [sales team](https://medusajs.com/contact/) to get help from the Medusa team.
-
-
-# Integrate Segment (Analytics) with Medusa
-
-In this tutorial, you'll learn how to integrate Segment with Medusa to track events and analytics.
-
-When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. Medusa's architecture facilitates integrating third-party services to customize Medusa's infrastructure for your business needs.
-
-To track analytics in your Medusa application, you can integrate [Segment](https://segment.com/), a service that collects analytics from multiple sources and sends them to various destinations. This tutorial will help you set up Segment in your Medusa application and track common events.
-
-## Summary
-
-By following this tutorial, you'll learn how to:
-
-- Install and set up Medusa.
-- Integrate Segment with your Medusa application.
-- Handle Medusa's `order.placed` event to track order placements.
-- Track custom events in your Medusa application with Segment.
-
-You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer.
-
-
-
-[Example Repository](https://github.com/medusajs/examples/tree/main/segment-integration): Find the full code of the guide in this repository.
-
-***
-
-## Step 1: Install a Medusa Application
-
-### Prerequisites
-
-- [Node.js v20+](https://nodejs.org/en/download)
-- [Git CLI tool](https://git-scm.com/downloads)
-- [PostgreSQL](https://www.postgresql.org/download/)
-
-Start by installing the Medusa application on your machine with the following command:
-
-```bash
-npx create-medusa-app@latest
-```
-
-First, you'll be asked for the project's name. Then, when prompted about installing the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose "Yes."
-
-Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name and the Next.js Starter Storefront in a separate directory named `{project-name}-storefront`.
-
-The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).
-
-Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterwards, you can log in with the new user and explore the dashboard.
-
-Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.
-
-***
-
-## Step 2: Create Segment Module Provider
-
-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's [Analytics Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/analytics/index.html.md) provides an interface to track events in your Medusa application. It delegates the actual tracking to the configured Analytics Module Provider.
-
-In this step, you'll integrate Segment as an Analytics Module Provider. Later, you'll use it to track events in your Medusa application.
-
-Refer to the [Modules](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) documentation to learn more about modules in Medusa.
-
-### a. Install Segment Node SDK
-
-Before you create the Segment Module Provider, you'll install the Segment Node SDK to interact with Segment's API.
-
-Run the following command in your Medusa application's directory:
-
-```bash npm2yarn
-npm install @segment/analytics-node
-```
-
-You'll use the SDK in the next steps.
-
-### b. Create Module Directory
-
-A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/segment`.
-
-### c. Create Segment Module's Service
-
-A module has a service that contains its logic. For Analytics Module Providers, the service implements the logic to track events in the third-party service.
-
-To create the service of the Segment Analytics Module Provider, create the file `src/modules/segment/service.ts` with the following content:
-
-```ts title="src/modules/segment/service.ts" highlights={serviceHighlights}
-import {
- AbstractAnalyticsProviderService,
- MedusaError,
-} from "@medusajs/framework/utils"
-import { Analytics } from "@segment/analytics-node"
-
-type Options = {
- writeKey: string
-}
-
-type InjectedDependencies = {}
-
-class SegmentAnalyticsProviderService extends AbstractAnalyticsProviderService {
- private client: Analytics
- static identifier = "segment"
-
- constructor(container: InjectedDependencies, options: Options) {
- super()
- if (!options.writeKey) {
- throw new MedusaError(
- MedusaError.Types.INVALID_DATA,
- "Segment write key is required"
- )
- }
- this.client = new Analytics({ writeKey: options.writeKey })
- }
-}
-
-export default SegmentAnalyticsProviderService
-```
-
-An Analytics Module Provider's service must extend the `AbstractAnalyticsProviderService` class. It must also have an `identifier` static property with the unique identifier of the provider.
-
-A module provider's constructor receives two parameters:
-
-- `container`: The [module's container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md) that contains Framework resources available to the module. In this tutorial, you don't need to resolve any resources.
-- `options`: Options that are passed to the module provider when it's registered in Medusa's configurations. You define the following option:
- - `writeKey`: The Segment write key. You'll learn how to retrieve and set this option in the [Add Module Provider to Medusa's Configurations](#h-add-module-provider-to-medusas-configurations) section.
-
-In the constructor, you create a Segment client using the Segment Node SDK. You pass the `writeKey` option to the client.
-
-You'll use this client to implement the service's methods in the next sections.
-
-Refer to the [Create Analytics Module Provider](https://docs.medusajs.com/references/analytics/provider/index.html.md) guide for detailed information about the methods.
-
-### d. Implement identify Method
-
-The `identify` method is used to identify a user in Segment. It associates the user's ID with their profile information, such as name and email.
-
-Add the `identify` method to the `SegmentAnalyticsProviderService` class:
-
-```ts title="src/modules/segment/service.ts"
-// other imports...
-import { ProviderIdentifyAnalyticsEventDTO } from "@medusajs/types"
-
-class SegmentAnalyticsProviderService extends AbstractAnalyticsProviderService {
- // ...
- async identify(data: ProviderIdentifyAnalyticsEventDTO): Promise {
- const anonymousId = data.properties && "anonymousId" in data.properties ?
- data.properties.anonymousId : undefined
- const traits = data.properties && "traits" in data.properties ?
- data.properties.traits : undefined
-
- if ("group" in data) {
- this.client.group({
- groupId: data.group.id,
- userId: data.actor_id,
- anonymousId,
- traits,
- context: data.properties
- })
- } else {
- this.client.identify({
- userId: data.actor_id,
- anonymousId,
- traits,
- context: data.properties
- })
- }
- }
-}
-```
-
-#### Parameters
-
-The `identify` method receives an object with the following properties:
-
-- `actor_id`: The ID of the user being identified.
-- `group`: Alternatively, the group being identified. If this property is present, the `actor_id` is ignored.
-- `properties`: Additional properties to associate with the user or group. This can include traits like name, email, and so on.
-
-The method receives other parameters, which you can find in the [Create Analytics Module Provider](https://docs.medusajs.com/references/analytics/provider#identify/index.html.md) guide.
-
-#### Method Logic
-
-In the method, if the `group` property is present, you call the `group` method of the Segment client to identify a group. Otherwise, you call the `identify` method to identify a user.
-
-For both methods, you extract the `anonymousId` and `traits` from the `properties` object if they are present. You also pass the `actor_id` as the `userId`, and `group.id` for groups.
-
-### e. Implement track Method
-
-The `track` method is used to track events in Segment. It can track events like order placements, cart updates, and more.
-
-Add the `track` method to the `SegmentAnalyticsProviderService` class:
-
-```ts title="src/modules/segment/service.ts"
-// other imports...
-import { ProviderTrackAnalyticsEventDTO } from "@medusajs/types"
-
-class SegmentAnalyticsProviderService extends AbstractAnalyticsProviderService {
- // ...
- async track(data: ProviderTrackAnalyticsEventDTO): Promise {
- const userId = "group" in data ?
- data.actor_id || data.group?.id : data.actor_id
- const anonymousId = data.properties && "anonymousId" in data.properties ?
- data.properties.anonymousId : undefined
-
- if (!userId && !anonymousId) {
- throw new MedusaError(
- MedusaError.Types.INVALID_DATA,
- `Actor or group ID is required for event ${data.event}`
- )
- }
-
- this.client.track({
- userId,
- anonymousId,
- event: data.event,
- properties: data.properties,
- timestamp: data.properties && "timestamp" in data.properties ?
- new Date(data.properties.timestamp) : undefined,
- })
- }
-}
-```
-
-#### Parameters
-
-The `track` method receives an object with the following properties:
-
-- `actor_id`: The ID of the user performing the event.
-- `group`: Alternatively, the group performing the event. If this property is present, the `actor_id` is ignored.
-- `event`: The name of the event being tracked.
-- `properties`: Additional properties associated with the event. This can include details like product ID, order ID, and so on.
-
-The method receives other parameters, which you can find in the [Create Analytics Module Provider](https://docs.medusajs.com/references/analytics/provider#track/index.html.md) guide.
-
-#### Method Logic
-
-In the method, you set the user ID either to the actor or group ID. You also check if the anonymous ID is present in the properties to use it.
-
-Next, you call the `track` method of the Segment client, passing it the user ID, anonymous ID, event name, properties, and timestamp (if present in the properties).
-
-### f. Implement shutdown Method
-
-The `shutdown` method is used to gracefully shut down the Segment client when the Medusa application is stopped. It allows you to send all pending events to Segment before the application exits.
-
-Add the following method to the `SegmentAnalyticsProviderService` class:
-
-```ts title="src/modules/segment/service.ts"
-class SegmentAnalyticsProviderService extends AbstractAnalyticsProviderService {
- // ...
- async shutdown(): Promise {
- await this.client.flush({
- close: true,
- })
- }
-}
-```
-
-#### Method Logic
-
-In the method, you call the `flush` method of the Segment client with the `close` option set to `true`. This method will send all pending events to Segment and close the client connection.
-
-### g. Export Module Definition
-
-You've now finished implementing the necessary methods for the Segment Analytics Module Provider.
-
-The final piece to a module is its definition, which you export in an `index.ts` file at the module's root directory. This definition tells Medusa the module's details, including its service.
-
-To create the module's definition, create the file `src/modules/segment/index.ts` with the following content:
-
-```ts title="src/modules/segment/index.ts"
-import SegmentAnalyticsProviderService from "./service"
-import {
- ModuleProvider,
- Modules,
-} from "@medusajs/framework/utils"
-
-export default ModuleProvider(Modules.ANALYTICS, {
- services: [SegmentAnalyticsProviderService],
-})
-```
-
-You use `ModuleProvider` from the Modules SDK to create the module provider's definition. It accepts two parameters:
-
-1. The name of the module that this provider belongs to, which is `Modules.ANALYTICS` in this case.
-2. An object with a required property `services` indicating the Module Provider's services.
-
-### h. Add Module Provider to Medusa's Configurations
-
-Once you finish building the module, add it to Medusa's configurations to start using it.
-
-In `medusa-config.ts`, add a `modules` property:
-
-```ts title="medusa-config.ts"
-module.exports = defineConfig({
- // ...
- modules: [
- {
- resolve: "@medusajs/medusa/analytics",
- options: {
- providers: [
- {
- resolve: "./src/modules/segment",
- id: "segment",
- options: {
- writeKey: process.env.SEGMENT_WRITE_KEY || "",
- },
- },
- ],
- },
- },
- ],
-})
-```
-
-To pass an Analytics Module Provider to the Analytics Module, you add the `modules` property to the Medusa configuration and pass the Analytics Module in its value.
-
-The Analytics Module accepts a `providers` option, which is an array of Analytics Module Providers to register. However, you can only register one analytics provider in your Medusa application.
-
-To register the Segment Analytics Module Provider, you add an object to the `providers` array with the following properties:
-
-- `resolve`: The NPM package or path to the module provider. In this case, it's the path to the `src/modules/segment` directory.
-- `id`: The ID of the module provider. The Analytics Module Provider is then registered with the ID `aly_{identifier}_{id}`, where:
- - `{identifier}`: The identifier static property defined in the Module Provider's service, which is `segment` in this case.
- - `{id}`: The ID set in this configuration, which is also `segment` in this case.
-- `options`: The options to pass to the module provider. These are the options you defined in the `Options` interface of the module provider's service.
-
-### i. Set Option as Environment Variable
-
-Next, you'll set the Segment write key as an environment variable.
-
-To retrieve the Segment write key:
-
-1. Log into your [Segment](https://app.segment.com) account.
-2. Go to the Connections page and click the "Add More" button next to the "Sources" section.
-
-
-
-3. In the "Choose a Source" step, select "Node.js" and click the "Next" button.
-
-
-
-4. In the "Connect your Node.js Source" step, enter a name for the source and click the "Create Source" button. This will show you the write key to copy.
-
-
-
-You can skip the next step of testing out the source for now.
-
-Then, add the following environment variable to your `.env` file:
-
-```shell
-SEGMENT_WRITE_KEY=123...
-```
-
-Replace `123...` with the write key you copied from Segment.
-
-You'll test out the integration as you set up event tracking in the next steps.
-
-***
-
-## Step 3: Track Order Placement Event
-
-You'll first track the order-placement event, which is triggered natively in the Medusa application.
-
-Medusa's events system allows you to listen to events triggered by the Medusa application and execute custom logic asynchronously in a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md).
-
-In the subscriber, you execute functionalities created in [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). A workflow is a series of actions, called steps, that complete a task.
-
-In this step, you'll create a workflow that tracks the `order.placed` event in Segment. Then, you'll create a subscriber that listens to this event and executes the workflow.
-
-### a. Create Track Event Step
-
-Before you create the workflow, you'll create a step that tracks an event in Segment. Later, you'll use this step in the workflows that track events, such as the order-placement event.
-
-To create a step, create the file `src/workflows/steps/track-event.ts` with the following content:
-
-```ts title="src/workflows/steps/track-event.ts" highlights={stepHighlights}
-import { createStep } from "@medusajs/framework/workflows-sdk"
-
-type TrackEventStepInput = {
- event: string
- userId?: string
- properties?: Record
- timestamp?: Date
-}
-
-export const trackEventStep = createStep(
- "track-event",
- async (input: TrackEventStepInput, { container }) => {
- const analyticsModuleService = container.resolve(
- "analytics"
- )
-
- if (!input.userId) {
- // generate a random user id
- input.properties = {
- ...input.properties,
- anonymousId: Math.random().toString(36).substring(2, 15) +
- Math.random().toString(36).substring(2, 15),
- }
- }
-
- await analyticsModuleService.track({
- event: input.event,
- actor_id: input.userId,
- properties: input.properties,
- })
- }
-)
-```
-
-You create a step with `createStep` from the Workflows SDK. It accepts two parameters:
-
-1. The step's unique name, which is `track-event`.
-2. An async function that receives two parameters:
- - The step's input, which is in this case an object with the following properties:
- - `event`: The name of the event to track.
- - `userId`: The ID of the user performing the event.
- - `properties`: Additional properties associated with the event.
- - `timestamp`: The timestamp of the event (optional).
- - 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.
-
-The Medusa container is different from the module's container. Since modules are isolated, they each have a container with their resources. Refer to the [Module Container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md) documentation for more information.
-
-In the step function, you resolve the Analytics Module's service from the Medusa container. This service is the interface to track events with the configured Analytics Module Provider, which is Segment in this case.
-
-If the `userId` is not provided, you generate a random anonymous ID and add it to the properties. This is useful for tracking events from users who are not logged in.
-
-Finally, you call the `track` method of the Analytics Module's service, passing it the event name, user ID, and properties.
-
-### b. Create Track Order Placed Workflow
-
-Next, you'll create the workflow that tracks the order placement event.
-
-To create the workflow, create the file `src/workflows/track-order-placed.ts` with the following content:
-
-```ts title="src/workflows/track-order-placed.ts" highlights={workflowHighlights}
-import {
- createWorkflow,
- transform,
-} from "@medusajs/framework/workflows-sdk"
-import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
-import { trackEventStep } from "./steps/track-event"
-
-type WorkflowInput = {
- id: string
-}
-
-export const trackOrderPlacedWorkflow = createWorkflow(
- "track-order-placed",
- ({ id }: WorkflowInput) => {
- // @ts-ignore
- const { data: orders } = useQueryGraphStep({
- entity: "order",
- fields: [
- "id",
- "email",
- "total",
- "currency_code",
- "items.*",
- "customer.id",
- "customer.email",
- "customer.first_name",
- "customer.last_name",
- "created_at",
- ],
- filters: {
- id,
- },
- })
-
- const order = transform({
- order: orders[0],
- }, ({ order }) => ({
- orderId: order.id,
- email: order.email,
- total: order.total,
- currency: order.currency_code,
- items: order.items?.map((item) => ({
- id: item?.id,
- title: item?.title,
- quantity: item?.quantity,
- variant: item?.variant,
- unit_price: item?.unit_price,
- })),
- customer: {
- id: order.customer?.id,
- email: order.customer?.email,
- firstName: order.customer?.first_name,
- lastName: order.customer?.last_name,
- },
- timestamp: order.created_at,
- }))
-
- trackEventStep({
- event: "order.placed",
- userId: order.customer?.id,
- properties: order,
- })
- }
-)
-```
-
-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 an object holding the ID of the order placed.
-
-In the workflow's constructor function, you:
-
-1. Retrieve the Medusa order using the `useQueryGraphStep` helper step. This step uses Medusa's [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) tool to retrieve data across modules. You pass it the order ID to retrieve.
-2. Use [transform](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) to prepare the tracking data, as direct data and variable manipulation isn't allowed in workflows. Learn more in the [Data Manipulation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) documentation.
-3. Send the tracking event to Segment using the `trackEventStep` you created in the previous step.
-
-You now have the workflow that tracks the order placement event.
-
-Refer to the [Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) documentation to learn more about workflows and steps.
-
-### c. Handle order.placed Event
-
-Next, you'll create a subscriber that listens to the `order.placed` event and executes the workflow you created in the previous step.
-
-To create the subscriber, create the file `src/subscribers/order-placed.ts` with the following content:
-
-```ts title="src/subscribers/order-placed.ts" highlights={subscriberHighlights}
-import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
-import { trackOrderPlacedWorkflow } from "../workflows/track-order-placed"
-
-export default async function orderPlacedHandler({
- event: { data },
- container,
-}: SubscriberArgs<{ id: string }>) {
- await trackOrderPlacedWorkflow(container)
- .run({
- input: {
- id: data.id,
- },
- })
-}
-
-export const config: SubscriberConfig = {
- event: "order.placed",
-}
-```
-
-A subscriber file must export:
-
-1. An asynchronous function, which is the subscriber that is executed when the event is emitted.
-2. A configuration object that holds the name of the event that the subscriber listens to, which is `order.placed` in this case.
-
-The subscriber function receives an object as a parameter that has the following properties:
-
-- `event`: An object that holds the event's data payload. The payload of the `order.placed` event is the ID of the order placed.
-- `container`: The Medusa container to access the Framework and commerce tools.
-
-In the subscriber function, you execute the `trackOrderPlacedWorkflow` by invoking it, passing the Medusa container as a parameter. Then, you chain a `run` method, passing it the order ID from the event's data payload as input.
-
-Refer to the [Events and Subscribers](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) documentation to learn more about creating subscribers.
-
-### Test it Out
-
-You'll now test out the segment integration by placing an order using the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md).
-
-The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.
-
-So, if your Medusa application's directory is `medusa-segment`, you can find the storefront by going back to the parent directory and changing to the `medusa-segment-storefront` directory:
-
-```bash
-cd ../medusa-segment-storefront # change based on your project name
-```
-
-First, run the following command in your Medusa application's directory to start the Medusa server:
-
-```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green"
-npm run dev
-```
-
-Then, run the following command in your Next.js Starter Storefront's directory to start the storefront:
-
-```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
-npm run dev
-```
-
-In the storefront, add a product to the cart and proceed to checkout. Once you place the order, open the Segment dashboard to view the order event:
-
-1. Go to Connections > Sources.
-2. Click on the Node.js source you created earlier.
-3. Click on the "Debugger" tab at the top of the page.
-4. You should see the `order.placed` event with the order details.
-
-The event may take a few seconds to appear in the debugger.
-
-
-
-***
-
-## Track Custom Event
-
-In your Medusa application, you often need to track custom events that are relevant to your business use case. For example, a B2B business may want to track whenever a user requests a quote.
-
-In Medusa, you can emit custom events in your workflows when an action occurs. Then, you can create a subscriber that listens to the custom event and executes a workflow to track it in Segment.
-
-For example, if you have a `createQuoteWorkflow`, you can use Medusa's [emitEventStep](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/emit-event#emit-event-in-a-workflow/index.html.md) to emit a custom event after the quote is created:
-
-```ts title="src/workflows/create-quote.ts"
-import {
- createWorkflow,
-} from "@medusajs/framework/workflows-sdk"
-import {
- emitEventStep,
-} from "@medusajs/medusa/core-flows"
-
-const createQuoteWorkflow = createWorkflow(
- "create-quote",
- () => {
- // ...
-
- emitEventStep({
- eventName: "quote.created",
- data: {
- id: "123",
- // other data payload
- },
- })
- }
-)
-```
-
-You can then create a subscriber that listens to the `quote.created` event and executes a workflow to track it in Segment:
-
-```ts title="src/subscribers/quote-created.ts"
-import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
-import { trackQuoteWorkflow } from "../workflows/track-order-placed"
-
-export default async function orderPlacedHandler({
- event: { data },
- container,
-}: SubscriberArgs<{ id: string }>) {
- await trackQuoteWorkflow(container)
- .run({
- input: {
- id: data.id,
- },
- })
-}
-
-export const config: SubscriberConfig = {
- event: "quote.created",
-}
-```
-
-The above example assumes you have a `trackQuoteWorkflow` that tracks the quote creation event in Segment, similar to the [trackOrderPlacedWorkflow](#b-create-track-order-placed-workflow) you created earlier.
-
-***
-
-## Next Steps
-
-You've now integrated Segment with your Medusa application and tracked common events like order placement. You can expand on the features in this tutorial to:
-
-- Track more events in your Medusa application, such as user sign-ups, cart additions, and more. You can refer to the [Events Reference](https://docs.medusajs.com/references/events/index.html.md) for a full list of events emitted by Medusa.
-- Emit custom events that are relevant for your business use case, and track them in Segment.
-- Add destinations to Segment to benefit from the data collected. Segment supports various destinations, such as Google Analytics, Metabase, and more.
-
-If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth understanding of all the concepts you've used in this guide and more.
-
-To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).
-
-### Troubleshooting
-
-If you encounter issues during your development, check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/index.html.md).
-
-### Getting Help
-
-If you encounter issues not covered in the troubleshooting guides:
-
-1. Visit the [Medusa GitHub repository](https://github.com/medusajs/medusa) to report issues or ask questions.
-2. Join the [Medusa Discord community](https://discord.gg/medusajs) for real-time support from community members.
-3. Contact the [sales team](https://medusajs.com/contact/) to get help from the Medusa team.
-
-
# How to Build a Wishlist Plugin
In this guide, you'll learn how to build a wishlist [plugin](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md) in Medusa.
@@ -65521,181 +65552,199 @@ To learn more about the commerce features that Medusa provides, check out Medusa
## JS SDK Admin
-- [batchPromotions](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.batchPromotions/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.create/index.html.md)
-- [delete](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.delete/index.html.md)
+- [batchPromotions](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.batchPromotions/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.list/index.html.md)
-- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.retrieve/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.update/index.html.md)
-- [batchSalesChannels](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.batchSalesChannels/index.html.md)
+- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.retrieve/index.html.md)
+- [delete](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.delete/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.delete/index.html.md)
-- [list](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.list/index.html.md)
-- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.retrieve/index.html.md)
-- [revoke](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.revoke/index.html.md)
-- [update](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.update/index.html.md)
+- [batchSalesChannels](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.batchSalesChannels/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.create/index.html.md)
+- [list](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.list/index.html.md)
+- [update](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.update/index.html.md)
+- [revoke](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.revoke/index.html.md)
+- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.retrieve/index.html.md)
+- [getItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.getItem/index.html.md)
+- [setItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.setItem/index.html.md)
+- [removeItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.removeItem/index.html.md)
- [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)
- [addInboundItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addInboundItems/index.html.md)
-- [addOutboundItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addOutboundItems/index.html.md)
- [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)
- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.cancel/index.html.md)
-- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.cancelRequest/index.html.md)
-- [deleteInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.deleteInboundShipping/index.html.md)
+- [addItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addItems/index.html.md)
+- [addOutboundItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addOutboundItems/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.create/index.html.md)
+- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.cancelRequest/index.html.md)
- [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)
- [removeOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeOutboundItem/index.html.md)
-- [updateInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateInboundItem/index.html.md)
-- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.retrieve/index.html.md)
+- [removeItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeItem/index.html.md)
+- [deleteInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.deleteInboundShipping/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)
- [updateInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateInboundShipping/index.html.md)
-- [updateOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateOutboundShipping/index.html.md)
- [updateItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateItem/index.html.md)
+- [updateInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateInboundItem/index.html.md)
+- [updateOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateOutboundShipping/index.html.md)
- [updateOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateOutboundItem/index.html.md)
+- [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)
+- [clearToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.clearToken_/index.html.md)
+- [fetch](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.fetch/index.html.md)
+- [getApiKeyHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getApiKeyHeader_/index.html.md)
+- [getJwtHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getJwtHeader_/index.html.md)
+- [getTokenStorageInfo\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getTokenStorageInfo_/index.html.md)
+- [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)
+- [getToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getToken_/index.html.md)
+- [setToken](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.setToken/index.html.md)
+- [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)
- [batchCustomerGroups](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.batchCustomerGroups/index.html.md)
-- [createAddress](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.createAddress/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.create/index.html.md)
-- [deleteAddress](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.deleteAddress/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.delete/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)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.list/index.html.md)
- [listAddresses](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.listAddresses/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.retrieve/index.html.md)
-- [update](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.update/index.html.md)
- [retrieveAddress](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.retrieveAddress/index.html.md)
+- [update](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.update/index.html.md)
- [updateAddress](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.updateAddress/index.html.md)
-- [setItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.setItem/index.html.md)
-- [getItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.getItem/index.html.md)
-- [removeItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.removeItem/index.html.md)
-- [clearToken](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.clearToken/index.html.md)
-- [clearToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.clearToken_/index.html.md)
-- [fetch](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.fetch/index.html.md)
-- [fetchStream](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.fetchStream/index.html.md)
-- [getJwtHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getJwtHeader_/index.html.md)
-- [getApiKeyHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getApiKeyHeader_/index.html.md)
-- [getPublishableKeyHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getPublishableKeyHeader_/index.html.md)
-- [getToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getToken_/index.html.md)
-- [initClient](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.initClient/index.html.md)
-- [setToken](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.setToken/index.html.md)
-- [getTokenStorageInfo\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getTokenStorageInfo_/index.html.md)
-- [setToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.setToken_/index.html.md)
-- [throwError\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.throwError_/index.html.md)
- [batchCustomers](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.batchCustomers/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.create/index.html.md)
-- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.retrieve/index.html.md)
-- [list](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.list/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.delete/index.html.md)
+- [list](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.list/index.html.md)
+- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.retrieve/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.update/index.html.md)
-- [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)
-- [confirmEdit](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.confirmEdit/index.html.md)
+- [addItems](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.addItems/index.html.md)
- [addShippingMethod](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.addShippingMethod/index.html.md)
- [addPromotions](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.addPromotions/index.html.md)
-- [addItems](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.addItems/index.html.md)
+- [beginEdit](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.beginEdit/index.html.md)
- [convertToOrder](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.convertToOrder/index.html.md)
+- [cancelEdit](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.cancelEdit/index.html.md)
+- [confirmEdit](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.confirmEdit/index.html.md)
- [removeActionShippingMethod](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.removeActionShippingMethod/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)
+- [removeActionItem](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.removeActionItem/index.html.md)
- [removePromotions](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.removePromotions/index.html.md)
- [removeShippingMethod](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.removeShippingMethod/index.html.md)
- [requestEdit](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.requestEdit/index.html.md)
-- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.retrieve/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.update/index.html.md)
-- [updateItem](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.updateItem/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)
-- [removeActionItem](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.removeActionItem/index.html.md)
-- [updateShippingMethod](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.updateShippingMethod/index.html.md)
-- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.cancel/index.html.md)
- [updateActionShippingMethod](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.updateActionShippingMethod/index.html.md)
+- [updateItem](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.updateItem/index.html.md)
+- [updateShippingMethod](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.updateShippingMethod/index.html.md)
+- [addInboundItems](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addInboundItems/index.html.md)
+- [addInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addInboundShipping/index.html.md)
+- [addOutboundItems](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addOutboundItems/index.html.md)
+- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.cancelRequest/index.html.md)
+- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.cancel/index.html.md)
+- [create](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.create/index.html.md)
+- [addOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addOutboundShipping/index.html.md)
+- [deleteInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.deleteInboundShipping/index.html.md)
+- [deleteOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.deleteOutboundShipping/index.html.md)
+- [removeOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.removeOutboundItem/index.html.md)
+- [list](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.list/index.html.md)
+- [removeInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.removeInboundItem/index.html.md)
+- [request](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.request/index.html.md)
+- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.retrieve/index.html.md)
+- [updateInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateInboundItem/index.html.md)
+- [updateOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateOutboundShipping/index.html.md)
+- [updateOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateOutboundItem/index.html.md)
+- [updateInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateInboundShipping/index.html.md)
+- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.cancel/index.html.md)
+- [create](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.create/index.html.md)
- [createShipment](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.createShipment/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentProvider/methods/js_sdk.admin.FulfillmentProvider.list/index.html.md)
-- [create](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.create/index.html.md)
-- [createServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.createServiceZone/index.html.md)
-- [delete](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.delete/index.html.md)
-- [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)
- [listFulfillmentOptions](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentProvider/methods/js_sdk.admin.FulfillmentProvider.listFulfillmentOptions/index.html.md)
-- [batchInventoryItemsLocationLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.batchInventoryItemsLocationLevels/index.html.md)
-- [create](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.create/index.html.md)
-- [retrieveServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.retrieveServiceZone/index.html.md)
- [batchInventoryItemLocationLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.batchInventoryItemLocationLevels/index.html.md)
+- [create](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.create/index.html.md)
+- [batchInventoryItemsLocationLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.batchInventoryItemsLocationLevels/index.html.md)
- [batchUpdateLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.batchUpdateLevels/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.delete/index.html.md)
- [deleteLevel](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.deleteLevel/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.list/index.html.md)
-- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.retrieve/index.html.md)
- [listLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.listLevels/index.html.md)
-- [update](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.update/index.html.md)
+- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.retrieve/index.html.md)
- [updateLevel](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.updateLevel/index.html.md)
+- [createServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.createServiceZone/index.html.md)
+- [update](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.update/index.html.md)
+- [delete](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.delete/index.html.md)
+- [updateServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.updateServiceZone/index.html.md)
+- [deleteServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.deleteServiceZone/index.html.md)
+- [retrieveServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.retrieveServiceZone/index.html.md)
- [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)
- [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)
- [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/Notification/methods/js_sdk.admin.Notification.list/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.retrieve/index.html.md)
+- [resend](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.resend/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Notification/methods/js_sdk.admin.Notification.retrieve/index.html.md)
-- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.cancelRequest/index.html.md)
-- [initiateRequest](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.initiateRequest/index.html.md)
-- [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)
-- [removeAddedItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.removeAddedItem/index.html.md)
-- [updateAddedItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.updateAddedItem/index.html.md)
-- [request](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.request/index.html.md)
-- [capture](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.capture/index.html.md)
-- [updateOriginalItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.updateOriginalItem/index.html.md)
-- [listPaymentProviders](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.listPaymentProviders/index.html.md)
-- [list](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.list/index.html.md)
-- [refund](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.refund/index.html.md)
-- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.retrieve/index.html.md)
-- [list](https://docs.medusajs.com/references/js_sdk/admin/Plugin/methods/js_sdk.admin.Plugin.list/index.html.md)
-- [createCreditLine](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.createCreditLine/index.html.md)
-- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancel/index.html.md)
+- [list](https://docs.medusajs.com/references/js_sdk/admin/Notification/methods/js_sdk.admin.Notification.list/index.html.md)
- [cancelTransfer](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancelTransfer/index.html.md)
+- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancel/index.html.md)
- [cancelFulfillment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancelFulfillment/index.html.md)
-- [createFulfillment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.createFulfillment/index.html.md)
+- [createCreditLine](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.createCreditLine/index.html.md)
- [createShipment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.createShipment/index.html.md)
+- [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)
- [listLineItems](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.listLineItems/index.html.md)
-- [markAsDelivered](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.markAsDelivered/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.list/index.html.md)
- [requestTransfer](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.requestTransfer/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)
- [retrievePreview](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.retrievePreview/index.html.md)
+- [capture](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.capture/index.html.md)
+- [update](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.update/index.html.md)
+- [list](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.list/index.html.md)
+- [refund](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.refund/index.html.md)
+- [listPaymentProviders](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.listPaymentProviders/index.html.md)
+- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.retrieve/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/PaymentCollection/methods/js_sdk.admin.PaymentCollection.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/PaymentCollection/methods/js_sdk.admin.PaymentCollection.delete/index.html.md)
- [markAsPaid](https://docs.medusajs.com/references/js_sdk/admin/PaymentCollection/methods/js_sdk.admin.PaymentCollection.markAsPaid/index.html.md)
-- [batchPrices](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.batchPrices/index.html.md)
+- [list](https://docs.medusajs.com/references/js_sdk/admin/Plugin/methods/js_sdk.admin.Plugin.list/index.html.md)
+- [addItems](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.addItems/index.html.md)
+- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.cancelRequest/index.html.md)
+- [confirm](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.confirm/index.html.md)
+- [initiateRequest](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.initiateRequest/index.html.md)
+- [removeAddedItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.removeAddedItem/index.html.md)
+- [request](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.request/index.html.md)
+- [updateAddedItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.updateAddedItem/index.html.md)
+- [updateOriginalItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.updateOriginalItem/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.create/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)
- [list](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.list/index.html.md)
-- [linkProducts](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.linkProducts/index.html.md)
-- [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/PriceList/methods/js_sdk.admin.PriceList.retrieve/index.html.md)
+- [linkProducts](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.linkProducts/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.update/index.html.md)
- [batchVariantInventoryItems](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batchVariantInventoryItems/index.html.md)
- [batch](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batch/index.html.md)
- [batchVariants](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batchVariants/index.html.md)
+- [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)
+- [createImport](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createImport/index.html.md)
- [createVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createVariant/index.html.md)
-- [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)
+- [delete](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.delete/index.html.md)
- [deleteVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.deleteVariant/index.html.md)
- [export](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.export/index.html.md)
-- [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)
+- [list](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.list/index.html.md)
- [import](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.import/index.html.md)
-- [confirmImport](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.confirmImport/index.html.md)
-- [createImport](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createImport/index.html.md)
-- [listVariants](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.listVariants/index.html.md)
-- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieve/index.html.md)
- [retrieveVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieveVariant/index.html.md)
+- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieve/index.html.md)
- [retrieveOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieveOption/index.html.md)
+- [listVariants](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.listVariants/index.html.md)
- [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)
@@ -65704,157 +65753,139 @@ To learn more about the commerce features that Medusa provides, check out Medusa
- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.delete/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.retrieve/index.html.md)
- [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)
-- [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)
-- [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)
- [create](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.create/index.html.md)
+- [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.updateProducts/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)
+- [list](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.list/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.update/index.html.md)
-- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.delete/index.html.md)
+- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.create/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.list/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.retrieve/index.html.md)
+- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.create/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/Promotion/methods/js_sdk.admin.Promotion.create/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)
-- [removeRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.removeRules/index.html.md)
-- [listRuleAttributes](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRuleAttributes/index.html.md)
-- [listRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRules/index.html.md)
-- [listRuleValues](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRuleValues/index.html.md)
-- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.retrieve/index.html.md)
-- [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)
-- [list](https://docs.medusajs.com/references/js_sdk/admin/RefundReason/methods/js_sdk.admin.RefundReason.list/index.html.md)
+- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.list/index.html.md)
+- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.delete/index.html.md)
+- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.update/index.html.md)
+- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.retrieve/index.html.md)
+- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.create/index.html.md)
+- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.delete/index.html.md)
+- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.list/index.html.md)
+- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.update/index.html.md)
+- [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.updateProducts/index.html.md)
+- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.retrieve/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductVariant/methods/js_sdk.admin.ProductVariant.list/index.html.md)
+- [create](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.create/index.html.md)
+- [delete](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.delete/index.html.md)
+- [list](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.list/index.html.md)
+- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.retrieve/index.html.md)
+- [addRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.addRules/index.html.md)
+- [create](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.create/index.html.md)
+- [delete](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.delete/index.html.md)
+- [list](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.list/index.html.md)
+- [listRuleAttributes](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRuleAttributes/index.html.md)
+- [update](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.update/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)
+- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.retrieve/index.html.md)
+- [update](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.update/index.html.md)
+- [updateRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.updateRules/index.html.md)
+- [list](https://docs.medusajs.com/references/js_sdk/admin/RefundReason/methods/js_sdk.admin.RefundReason.list/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.delete/index.html.md)
-- [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/Region/methods/js_sdk.admin.Region.list/index.html.md)
-- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.retrieve/index.html.md)
-- [list](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.list/index.html.md)
-- [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)
-- [update](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.update/index.html.md)
-- [addInboundItems](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addInboundItems/index.html.md)
-- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.cancel/index.html.md)
-- [addInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addInboundShipping/index.html.md)
-- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.cancelRequest/index.html.md)
-- [addOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addOutboundShipping/index.html.md)
-- [addOutboundItems](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addOutboundItems/index.html.md)
-- [deleteInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.deleteInboundShipping/index.html.md)
-- [create](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.create/index.html.md)
-- [deleteOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.deleteOutboundShipping/index.html.md)
-- [list](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.list/index.html.md)
-- [removeOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.removeOutboundItem/index.html.md)
-- [request](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.request/index.html.md)
-- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.retrieve/index.html.md)
-- [removeInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.removeInboundItem/index.html.md)
-- [updateInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateInboundItem/index.html.md)
-- [updateInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateInboundShipping/index.html.md)
-- [updateOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateOutboundItem/index.html.md)
-- [delete](https://docs.medusajs.com/references/js_sdk/admin/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)
-- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.create/index.html.md)
-- [updateOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateOutboundShipping/index.html.md)
-- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.retrieve/index.html.md)
-- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.update/index.html.md)
+- [update](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.update/index.html.md)
+- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.retrieve/index.html.md)
+- [addReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.addReturnItem/index.html.md)
+- [cancelReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancelReceive/index.html.md)
+- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancel/index.html.md)
+- [addReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.addReturnShipping/index.html.md)
+- [confirmReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.confirmReceive/index.html.md)
+- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancelRequest/index.html.md)
+- [deleteReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.deleteReturnShipping/index.html.md)
+- [dismissItems](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.dismissItems/index.html.md)
+- [confirmRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.confirmRequest/index.html.md)
+- [initiateRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.initiateRequest/index.html.md)
+- [initiateReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.initiateReceive/index.html.md)
+- [removeDismissItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeDismissItem/index.html.md)
+- [list](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.list/index.html.md)
+- [removeReceiveItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeReceiveItem/index.html.md)
+- [removeReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeReturnItem/index.html.md)
+- [updateDismissItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateDismissItem/index.html.md)
+- [updateReceiveItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReceiveItem/index.html.md)
+- [receiveItems](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.receiveItems/index.html.md)
+- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.retrieve/index.html.md)
+- [updateRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateRequest/index.html.md)
+- [updateReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReturnItem/index.html.md)
+- [updateReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReturnShipping/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.delete/index.html.md)
-- [list](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.list/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.retrieve/index.html.md)
-- [batchProducts](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.batchProducts/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.update/index.html.md)
+- [batchProducts](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.batchProducts/index.html.md)
- [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)
- [list](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.list/index.html.md)
+- [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.updateProducts/index.html.md)
+- [list](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.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)
-- [delete](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.delete/index.html.md)
-- [create](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.create/index.html.md)
-- [list](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.list/index.html.md)
-- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.retrieve/index.html.md)
-- [update](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.update/index.html.md)
-- [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)
-- [create](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.create/index.html.md)
-- [update](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.update/index.html.md)
-- [updateRules](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.updateRules/index.html.md)
-- [create](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.create/index.html.md)
- [createFulfillmentSet](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.createFulfillmentSet/index.html.md)
+- [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)
- [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)
-- [addReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.addReturnItem/index.html.md)
-- [cancelReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancelReceive/index.html.md)
-- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancel/index.html.md)
-- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancelRequest/index.html.md)
-- [confirmReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.confirmReceive/index.html.md)
-- [dismissItems](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.dismissItems/index.html.md)
-- [addReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.addReturnShipping/index.html.md)
-- [deleteReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.deleteReturnShipping/index.html.md)
-- [confirmRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.confirmRequest/index.html.md)
-- [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)
-- [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)
-- [list](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.list/index.html.md)
-- [removeReceiveItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeReceiveItem/index.html.md)
-- [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)
-- [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)
-- [updateReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReturnShipping/index.html.md)
-- [updateReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReturnItem/index.html.md)
+- [delete](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.delete/index.html.md)
+- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.retrieve/index.html.md)
+- [list](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.list/index.html.md)
+- [update](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.update/index.html.md)
+- [updateRules](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.updateRules/index.html.md)
+- [create](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.create/index.html.md)
+- [create](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.create/index.html.md)
+- [list](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.list/index.html.md)
+- [delete](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.delete/index.html.md)
+- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.retrieve/index.html.md)
+- [update](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.update/index.html.md)
- [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)
-- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.retrieve/index.html.md)
-- [update](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.update/index.html.md)
-- [list](https://docs.medusajs.com/references/js_sdk/admin/TaxProvider/methods/js_sdk.admin.TaxProvider.list/index.html.md)
+- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Store/methods/js_sdk.admin.Store.retrieve/index.html.md)
+- [delete](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.delete/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.create/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.list/index.html.md)
-- [delete](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.delete/index.html.md)
-- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.retrieve/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.update/index.html.md)
-- [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/TaxRegion/methods/js_sdk.admin.TaxRegion.retrieve/index.html.md)
+- [list](https://docs.medusajs.com/references/js_sdk/admin/TaxProvider/methods/js_sdk.admin.TaxProvider.list/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.create/index.html.md)
- [presignedUrl](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.presignedUrl/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)
+- [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)
+- [update](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.update/index.html.md)
+- [list](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.list/index.html.md)
+- [delete](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.delete/index.html.md)
+- [me](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.me/index.html.md)
+- [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/User/methods/js_sdk.admin.User.list/index.html.md)
+- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.retrieve/index.html.md)
+- [update](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.update/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/WorkflowExecution/methods/js_sdk.admin.WorkflowExecution.retrieve/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/WorkflowExecution/methods/js_sdk.admin.WorkflowExecution.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)
-- [update](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.update/index.html.md)
-- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.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)
-- [register](https://docs.medusajs.com/references/js-sdk/auth/register/index.html.md)
-- [refresh](https://docs.medusajs.com/references/js-sdk/auth/refresh/index.html.md)
- [resetPassword](https://docs.medusajs.com/references/js-sdk/auth/resetPassword/index.html.md)
+- [register](https://docs.medusajs.com/references/js-sdk/auth/register/index.html.md)
- [logout](https://docs.medusajs.com/references/js-sdk/auth/logout/index.html.md)
+- [refresh](https://docs.medusajs.com/references/js-sdk/auth/refresh/index.html.md)
- [updateProvider](https://docs.medusajs.com/references/js-sdk/auth/updateProvider/index.html.md)
@@ -65862,11 +65893,11 @@ To learn more about the commerce features that Medusa provides, check out Medusa
- [cart](https://docs.medusajs.com/references/js-sdk/store/cart/index.html.md)
- [category](https://docs.medusajs.com/references/js-sdk/store/category/index.html.md)
-- [collection](https://docs.medusajs.com/references/js-sdk/store/collection/index.html.md)
-- [order](https://docs.medusajs.com/references/js-sdk/store/order/index.html.md)
- [fulfillment](https://docs.medusajs.com/references/js-sdk/store/fulfillment/index.html.md)
- [customer](https://docs.medusajs.com/references/js-sdk/store/customer/index.html.md)
+- [order](https://docs.medusajs.com/references/js-sdk/store/order/index.html.md)
- [payment](https://docs.medusajs.com/references/js-sdk/store/payment/index.html.md)
+- [collection](https://docs.medusajs.com/references/js-sdk/store/collection/index.html.md)
- [product](https://docs.medusajs.com/references/js-sdk/store/product/index.html.md)
- [region](https://docs.medusajs.com/references/js-sdk/store/region/index.html.md)
@@ -66320,493 +66351,6 @@ export default ProductWidget
This widget also uses a [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) custom component.
-# Data Table - Admin Components
-
-This component is available after [Medusa v2.4.0+](https://github.com/medusajs/medusa/releases/tag/v2.4.0).
-
-The [DataTable component in Medusa UI](https://docs.medusajs.com/ui/components/data-table/index.html.md) allows you to display data in a table with sorting, filtering, and pagination. It's used across the Medusa Admin dashboard to showcase a list of items, such as a list of products.
-
-
-
-You can use this component in your Admin Extensions to display data in a table format, especially if you're retrieving them from API routes of the Medusa application.
-
-This guide focuses on how to use the `DataTable` component while fetching data from the backend. Refer to the [Medusa UI documentation](https://docs.medusajs.com/ui/components/data-table/index.html.md) for detailed information about the DataTable component and its different usages.
-
-## Example: DataTable with Data Fetching
-
-In this example, you'll create a UI widget that shows the list of products retrieved from the [List Products API Route](https://docs.medusajs.com/api/admin#products_getproducts) in a data table with pagination, filtering, searching, and sorting.
-
-Start by initializing the columns in the data table. To do that, use the `createDataTableColumnHelper` from Medusa UI:
-
-```tsx title="src/admin/routes/custom/page.tsx"
-import {
- createDataTableColumnHelper,
-} from "@medusajs/ui"
-import {
- HttpTypes,
-} from "@medusajs/framework/types"
-
-const columnHelper = createDataTableColumnHelper()
-
-const columns = [
- columnHelper.accessor("title", {
- header: "Title",
- // Enables sorting for the column.
- enableSorting: true,
- // If omitted, the header will be used instead if it's a string,
- // otherwise the accessor key (id) will be used.
- sortLabel: "Title",
- // If omitted the default value will be "A-Z"
- sortAscLabel: "A-Z",
- // If omitted the default value will be "Z-A"
- sortDescLabel: "Z-A",
- }),
- columnHelper.accessor("status", {
- header: "Status",
- cell: ({ getValue }) => {
- const status = getValue()
- return (
-
- {status === "published" ? "Published" : "Draft"}
-
- )
- },
- }),
-]
-```
-
-`createDataTableColumnHelper` utility creates a column helper that helps you define the columns for the data table. The column helper has an `accessor` method that accepts two parameters:
-
-1. The column's key in the table's data.
-2. An object with the following properties:
- - `header`: The column's header.
- - `cell`: (optional) By default, a data's value for a column is displayed as a string. Use this property to specify custom rendering of the value. It accepts a function that returns a string or a React node. The function receives an object that has a `getValue` property function to retrieve the raw value of the cell.
- - `enableSorting`: (optional) A boolean that enables sorting data by this column.
- - `sortLabel`: (optional) The label for the sorting button. If omitted, the `header` will be used instead if it's a string, otherwise the accessor key (id) will be used.
- - `sortAscLabel`: (optional) The label for the ascending sorting button. If omitted, the default value will be "A-Z".
- - `sortDescLabel`: (optional) The label for the descending sorting button. If omitted, the default value will be "Z-A".
-
-Next, you'll define the filters that can be applied to the data table. You'll configure filtering by product status.
-
-To define the filters, add the following:
-
-```tsx title="src/admin/routes/custom/page.tsx"
-// other imports...
-import {
- // ...
- createDataTableFilterHelper,
-} from "@medusajs/ui"
-
-const filterHelper = createDataTableFilterHelper()
-
-const filters = [
- filterHelper.accessor("status", {
- type: "select",
- label: "Status",
- options: [
- {
- label: "Published",
- value: "published",
- },
- {
- label: "Draft",
- value: "draft",
- },
- ],
- }),
-]
-```
-
-`createDataTableFilterHelper` utility creates a filter helper that helps you define the filters for the data table. The filter helper has an `accessor` method that accepts two parameters:
-
-1. The key of a column in the table's data.
-2. An object with the following properties:
- - `type`: The type of filter. It can be either:
- - `select`: A select dropdown allowing users to choose multiple values.
- - `radio`: A radio button allowing users to choose one value.
- - `date`: A date picker allowing users to choose a date.
- - `label`: The filter's label.
- - `options`: An array of objects with `label` and `value` properties. The `label` is the option's label, and the `value` is the value to filter by.
-
-You'll now start creating the UI widget's component. Start by adding the necessary state variables:
-
-```tsx title="src/admin/routes/custom/page.tsx"
-// other imports...
-import {
- // ...
- DataTablePaginationState,
- DataTableFilteringState,
- DataTableSortingState,
-} from "@medusajs/ui"
-import { useMemo, useState } from "react"
-
-// ...
-
-const limit = 15
-
-const CustomPage = () => {
- const [pagination, setPagination] = useState({
- pageSize: limit,
- pageIndex: 0,
- })
- const [search, setSearch] = useState("")
- const [filtering, setFiltering] = useState({})
- const [sorting, setSorting] = useState(null)
-
- const offset = useMemo(() => {
- return pagination.pageIndex * limit
- }, [pagination])
- const statusFilters = useMemo(() => {
- return (filtering.status || []) as ProductStatus
- }, [filtering])
-
- // TODO add data fetching logic
-}
-```
-
-In the component, you've added the following state variables:
-
-- `pagination`: An object of type `DataTablePaginationState` that holds the pagination state. It has two properties:
- - `pageSize`: The number of items to show per page.
- - `pageIndex`: The current page index.
-- `search`: A string that holds the search query.
-- `filtering`: An object of type `DataTableFilteringState` that holds the filtering state.
-- `sorting`: An object of type `DataTableSortingState` that holds the sorting state.
-
-You've also added two memoized variables:
-
-- `offset`: How many items to skip when fetching data based on the current page.
-- `statusFilters`: The selected status filters, if any.
-
-Next, you'll fetch the products from the Medusa application. Assuming you have the JS SDK configured as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md), add the following imports at the top of the file:
-
-```tsx title="src/admin/routes/custom/page.tsx"
-import { sdk } from "../../lib/config"
-import { useQuery } from "@tanstack/react-query"
-```
-
-This imports the JS SDK instance and `useQuery` from [Tanstack Query](https://tanstack.com/query/latest).
-
-Then, replace the `TODO` in the component with the following:
-
-```tsx title="src/admin/routes/custom/page.tsx"
-const { data, isLoading } = useQuery({
- queryFn: () => sdk.admin.product.list({
- limit,
- offset,
- q: search,
- status: statusFilters,
- order: sorting ? `${sorting.desc ? "-" : ""}${sorting.id}` : undefined,
- }),
- queryKey: [["products", limit, offset, search, statusFilters, sorting?.id, sorting?.desc]],
-})
-
-// TODO configure data table
-```
-
-You use the `useQuery` hook to fetch the products from the Medusa application. In the `queryFn`, you call the `sdk.admin.product.list` method to fetch the products. You pass the following query parameters to the method:
-
-- `limit`: The number of products to fetch per page.
-- `offset`: The number of products to skip based on the current page.
-- `q`: The search query, if set.
-- `status`: The status filters, if set.
-- `order`: The sorting order, if set.
-
-So, whenever the user changes the current page, search query, status filters, or sorting, the products are fetched based on the new parameters.
-
-Next, you'll configure the data table. Medusa UI provides a `useDataTable` hook that helps you configure the data table. Add the following imports at the top of the file:
-
-```tsx title="src/admin/routes/custom/page.tsx"
-import {
- // ...
- useDataTable,
-} from "@medusajs/ui"
-import { useNavigate } from "react-router-dom"
-```
-
-Then, replace the `TODO` in the component with the following:
-
-```tsx title="src/admin/routes/custom/page.tsx"
-const navigate = useNavigate()
-
-const table = useDataTable({
- columns,
- data: data?.products || [],
- getRowId: (row) => row.id,
- rowCount: data?.count || 0,
- isLoading,
- pagination: {
- state: pagination,
- onPaginationChange: setPagination,
- },
- search: {
- state: search,
- onSearchChange: setSearch,
- },
- filtering: {
- state: filtering,
- onFilteringChange: setFiltering,
- },
- filters,
- sorting: {
- // Pass the pagination state and updater to the table instance
- state: sorting,
- onSortingChange: setSorting,
- },
- onRowClick: (event, row) => {
- // Handle row click, for example
- navigate(`/products/${row.id}`)
- },
-})
-
-// TODO render component
-```
-
-The `useDataTable` hook accepts an object with the following properties:
-
-- columns: (\`array\`) The columns to display in the data table. You created this using the \`createDataTableColumnHelper\` utility.
-- data: (\`array\`) The products fetched from the Medusa application.
-- getRowId: (\`function\`) A function that returns the unique ID of a row.
-- rowCount: (\`number\`) The total number of products that can be retrieved. This is used to determine the number of pages.
-- isLoading: (\`boolean\`) A boolean that indicates if the data is being fetched.
-- pagination: (\`object\`) An object to configure pagination.
-
- - state: (\`object\`) The pagination React state variable.
-
- - onPaginationChange: (\`function\`) A function that updates the pagination state.
-- search: (\`object\`) An object to configure searching.
-
- - state: (\`string\`) The search query React state variable.
-
- - onSearchChange: (\`function\`) A function that updates the search query state.
-- filtering: (\`object\`) An object to configure filtering.
-
- - state: (\`object\`) The filtering React state variable.
-
- - onFilteringChange: (\`function\`) A function that updates the filtering state.
-- filters: (\`array\`) The filters to display in the data table. You created this using the \`createDataTableFilterHelper\` utility.
-- sorting: (\`object\`) An object to configure sorting.
-
- - state: (\`object\`) The sorting React state variable.
-
- - onSortingChange: (\`function\`) A function that updates the sorting state.
-- onRowClick: (\`function\`) A function that allows you to perform an action when the user clicks on a row. In this example, you navigate to the product's detail page.
-
- - event: (\`mouseevent\`) An instance of the \[MouseClickEvent]\(https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent) object.
-
- - row: (\`object\`) The data of the row that was clicked.
-
-Finally, you'll render the data table. But first, add the following imports at the top of the page:
-
-```tsx title="src/admin/routes/custom/page.tsx"
-import {
- // ...
- DataTable,
-} from "@medusajs/ui"
-import { SingleColumnLayout } from "../../layouts/single-column"
-import { Container } from "../../components/container"
-```
-
-Aside from the `DataTable` component, you also import the [SingleColumnLayout](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/layouts/single-column/index.html.md) and [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) components implemented in other Admin Component guides. These components ensure a style consistent to other pages in the admin dashboard.
-
-Then, replace the `TODO` in the component with the following:
-
-```tsx title="src/admin/routes/custom/page.tsx"
-return (
-
-
-
-
- Products
-
-
-
-
-
-
-
-
-
-
-
-)
-```
-
-You render the `DataTable` component and pass the `table` instance as a prop. In the `DataTable` component, you render a toolbar showing a heading, filter menu, sorting menu, and a search input. You also show pagination after the table.
-
-Lastly, export the component and the UI widget's configuration at the end of the file:
-
-```tsx title="src/admin/routes/custom/page.tsx"
-// other imports...
-import { defineRouteConfig } from "@medusajs/admin-sdk"
-import { ChatBubbleLeftRight } from "@medusajs/icons"
-
-// ...
-
-export const config = defineRouteConfig({
- label: "Custom",
- icon: ChatBubbleLeftRight,
-})
-
-export default CustomPage
-```
-
-If you start your Medusa application and go to `localhost:9000/app/custom`, you'll see the data table showing the list of products with pagination, filtering, searching, and sorting functionalities.
-
-### Full Example Code
-
-```tsx title="src/admin/routes/custom/page.tsx"
-import { defineRouteConfig } from "@medusajs/admin-sdk"
-import { ChatBubbleLeftRight } from "@medusajs/icons"
-import {
- Badge,
- createDataTableColumnHelper,
- createDataTableFilterHelper,
- DataTable,
- DataTableFilteringState,
- DataTablePaginationState,
- DataTableSortingState,
- Heading,
- useDataTable,
-} from "@medusajs/ui"
-import { useQuery } from "@tanstack/react-query"
-import { SingleColumnLayout } from "../../layouts/single-column"
-import { sdk } from "../../lib/config"
-import { useMemo, useState } from "react"
-import { Container } from "../../components/container"
-import { HttpTypes, ProductStatus } from "@medusajs/framework/types"
-
-const columnHelper = createDataTableColumnHelper()
-
-const columns = [
- columnHelper.accessor("title", {
- header: "Title",
- // Enables sorting for the column.
- enableSorting: true,
- // If omitted, the header will be used instead if it's a string,
- // otherwise the accessor key (id) will be used.
- sortLabel: "Title",
- // If omitted the default value will be "A-Z"
- sortAscLabel: "A-Z",
- // If omitted the default value will be "Z-A"
- sortDescLabel: "Z-A",
- }),
- columnHelper.accessor("status", {
- header: "Status",
- cell: ({ getValue }) => {
- const status = getValue()
- return (
-
- {status === "published" ? "Published" : "Draft"}
-
- )
- },
- }),
-]
-
-const filterHelper = createDataTableFilterHelper()
-
-const filters = [
- filterHelper.accessor("status", {
- type: "select",
- label: "Status",
- options: [
- {
- label: "Published",
- value: "published",
- },
- {
- label: "Draft",
- value: "draft",
- },
- ],
- }),
-]
-
-const limit = 15
-
-const CustomPage = () => {
- const [pagination, setPagination] = useState({
- pageSize: limit,
- pageIndex: 0,
- })
- const [search, setSearch] = useState("")
- const [filtering, setFiltering] = useState({})
- const [sorting, setSorting] = useState(null)
-
- const offset = useMemo(() => {
- return pagination.pageIndex * limit
- }, [pagination])
- const statusFilters = useMemo(() => {
- return (filtering.status || []) as ProductStatus
- }, [filtering])
-
- const { data, isLoading } = useQuery({
- queryFn: () => sdk.admin.product.list({
- limit,
- offset,
- q: search,
- status: statusFilters,
- order: sorting ? `${sorting.desc ? "-" : ""}${sorting.id}` : undefined,
- }),
- queryKey: [["products", limit, offset, search, statusFilters, sorting?.id, sorting?.desc]],
- })
-
- const table = useDataTable({
- columns,
- data: data?.products || [],
- getRowId: (row) => row.id,
- rowCount: data?.count || 0,
- isLoading,
- pagination: {
- state: pagination,
- onPaginationChange: setPagination,
- },
- search: {
- state: search,
- onSearchChange: setSearch,
- },
- filtering: {
- state: filtering,
- onFilteringChange: setFiltering,
- },
- filters,
- sorting: {
- // Pass the pagination state and updater to the table instance
- state: sorting,
- onSortingChange: setSorting,
- },
- })
-
- return (
-
-
-
-
- Products
-
-
-
-
-
-
-
-
-
-
-
- )
-}
-
-export const config = defineRouteConfig({
- label: "Custom",
- icon: ChatBubbleLeftRight,
-})
-
-export default CustomPage
-```
-
-
# Forms - Admin Components
The Medusa Admin has two types of forms:
@@ -67380,6 +66924,98 @@ 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.
+# Section Row - Admin Components
+
+The Medusa Admin often shows information in rows of label-values, such as when showing a product's details.
+
+
+
+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 (
+
+ )
+}
+```
+
+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.
+
+
# JSON View - Admin Components
Detail pages in the Medusa Admin show a JSON section to view the current page's details in JSON format.
@@ -67608,6 +67244,493 @@ export default ProductWidget
This shows the JSON section at the top of the product page, passing it the object `{ name: "John" }`.
+# Data Table - Admin Components
+
+This component is available after [Medusa v2.4.0+](https://github.com/medusajs/medusa/releases/tag/v2.4.0).
+
+The [DataTable component in Medusa UI](https://docs.medusajs.com/ui/components/data-table/index.html.md) allows you to display data in a table with sorting, filtering, and pagination. It's used across the Medusa Admin dashboard to showcase a list of items, such as a list of products.
+
+
+
+You can use this component in your Admin Extensions to display data in a table format, especially if you're retrieving them from API routes of the Medusa application.
+
+This guide focuses on how to use the `DataTable` component while fetching data from the backend. Refer to the [Medusa UI documentation](https://docs.medusajs.com/ui/components/data-table/index.html.md) for detailed information about the DataTable component and its different usages.
+
+## Example: DataTable with Data Fetching
+
+In this example, you'll create a UI widget that shows the list of products retrieved from the [List Products API Route](https://docs.medusajs.com/api/admin#products_getproducts) in a data table with pagination, filtering, searching, and sorting.
+
+Start by initializing the columns in the data table. To do that, use the `createDataTableColumnHelper` from Medusa UI:
+
+```tsx title="src/admin/routes/custom/page.tsx"
+import {
+ createDataTableColumnHelper,
+} from "@medusajs/ui"
+import {
+ HttpTypes,
+} from "@medusajs/framework/types"
+
+const columnHelper = createDataTableColumnHelper()
+
+const columns = [
+ columnHelper.accessor("title", {
+ header: "Title",
+ // Enables sorting for the column.
+ enableSorting: true,
+ // If omitted, the header will be used instead if it's a string,
+ // otherwise the accessor key (id) will be used.
+ sortLabel: "Title",
+ // If omitted the default value will be "A-Z"
+ sortAscLabel: "A-Z",
+ // If omitted the default value will be "Z-A"
+ sortDescLabel: "Z-A",
+ }),
+ columnHelper.accessor("status", {
+ header: "Status",
+ cell: ({ getValue }) => {
+ const status = getValue()
+ return (
+
+ {status === "published" ? "Published" : "Draft"}
+
+ )
+ },
+ }),
+]
+```
+
+`createDataTableColumnHelper` utility creates a column helper that helps you define the columns for the data table. The column helper has an `accessor` method that accepts two parameters:
+
+1. The column's key in the table's data.
+2. An object with the following properties:
+ - `header`: The column's header.
+ - `cell`: (optional) By default, a data's value for a column is displayed as a string. Use this property to specify custom rendering of the value. It accepts a function that returns a string or a React node. The function receives an object that has a `getValue` property function to retrieve the raw value of the cell.
+ - `enableSorting`: (optional) A boolean that enables sorting data by this column.
+ - `sortLabel`: (optional) The label for the sorting button. If omitted, the `header` will be used instead if it's a string, otherwise the accessor key (id) will be used.
+ - `sortAscLabel`: (optional) The label for the ascending sorting button. If omitted, the default value will be "A-Z".
+ - `sortDescLabel`: (optional) The label for the descending sorting button. If omitted, the default value will be "Z-A".
+
+Next, you'll define the filters that can be applied to the data table. You'll configure filtering by product status.
+
+To define the filters, add the following:
+
+```tsx title="src/admin/routes/custom/page.tsx"
+// other imports...
+import {
+ // ...
+ createDataTableFilterHelper,
+} from "@medusajs/ui"
+
+const filterHelper = createDataTableFilterHelper()
+
+const filters = [
+ filterHelper.accessor("status", {
+ type: "select",
+ label: "Status",
+ options: [
+ {
+ label: "Published",
+ value: "published",
+ },
+ {
+ label: "Draft",
+ value: "draft",
+ },
+ ],
+ }),
+]
+```
+
+`createDataTableFilterHelper` utility creates a filter helper that helps you define the filters for the data table. The filter helper has an `accessor` method that accepts two parameters:
+
+1. The key of a column in the table's data.
+2. An object with the following properties:
+ - `type`: The type of filter. It can be either:
+ - `select`: A select dropdown allowing users to choose multiple values.
+ - `radio`: A radio button allowing users to choose one value.
+ - `date`: A date picker allowing users to choose a date.
+ - `label`: The filter's label.
+ - `options`: An array of objects with `label` and `value` properties. The `label` is the option's label, and the `value` is the value to filter by.
+
+You'll now start creating the UI widget's component. Start by adding the necessary state variables:
+
+```tsx title="src/admin/routes/custom/page.tsx"
+// other imports...
+import {
+ // ...
+ DataTablePaginationState,
+ DataTableFilteringState,
+ DataTableSortingState,
+} from "@medusajs/ui"
+import { useMemo, useState } from "react"
+
+// ...
+
+const limit = 15
+
+const CustomPage = () => {
+ const [pagination, setPagination] = useState({
+ pageSize: limit,
+ pageIndex: 0,
+ })
+ const [search, setSearch] = useState("")
+ const [filtering, setFiltering] = useState({})
+ const [sorting, setSorting] = useState(null)
+
+ const offset = useMemo(() => {
+ return pagination.pageIndex * limit
+ }, [pagination])
+ const statusFilters = useMemo(() => {
+ return (filtering.status || []) as ProductStatus
+ }, [filtering])
+
+ // TODO add data fetching logic
+}
+```
+
+In the component, you've added the following state variables:
+
+- `pagination`: An object of type `DataTablePaginationState` that holds the pagination state. It has two properties:
+ - `pageSize`: The number of items to show per page.
+ - `pageIndex`: The current page index.
+- `search`: A string that holds the search query.
+- `filtering`: An object of type `DataTableFilteringState` that holds the filtering state.
+- `sorting`: An object of type `DataTableSortingState` that holds the sorting state.
+
+You've also added two memoized variables:
+
+- `offset`: How many items to skip when fetching data based on the current page.
+- `statusFilters`: The selected status filters, if any.
+
+Next, you'll fetch the products from the Medusa application. Assuming you have the JS SDK configured as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md), add the following imports at the top of the file:
+
+```tsx title="src/admin/routes/custom/page.tsx"
+import { sdk } from "../../lib/config"
+import { useQuery } from "@tanstack/react-query"
+```
+
+This imports the JS SDK instance and `useQuery` from [Tanstack Query](https://tanstack.com/query/latest).
+
+Then, replace the `TODO` in the component with the following:
+
+```tsx title="src/admin/routes/custom/page.tsx"
+const { data, isLoading } = useQuery({
+ queryFn: () => sdk.admin.product.list({
+ limit,
+ offset,
+ q: search,
+ status: statusFilters,
+ order: sorting ? `${sorting.desc ? "-" : ""}${sorting.id}` : undefined,
+ }),
+ queryKey: [["products", limit, offset, search, statusFilters, sorting?.id, sorting?.desc]],
+})
+
+// TODO configure data table
+```
+
+You use the `useQuery` hook to fetch the products from the Medusa application. In the `queryFn`, you call the `sdk.admin.product.list` method to fetch the products. You pass the following query parameters to the method:
+
+- `limit`: The number of products to fetch per page.
+- `offset`: The number of products to skip based on the current page.
+- `q`: The search query, if set.
+- `status`: The status filters, if set.
+- `order`: The sorting order, if set.
+
+So, whenever the user changes the current page, search query, status filters, or sorting, the products are fetched based on the new parameters.
+
+Next, you'll configure the data table. Medusa UI provides a `useDataTable` hook that helps you configure the data table. Add the following imports at the top of the file:
+
+```tsx title="src/admin/routes/custom/page.tsx"
+import {
+ // ...
+ useDataTable,
+} from "@medusajs/ui"
+import { useNavigate } from "react-router-dom"
+```
+
+Then, replace the `TODO` in the component with the following:
+
+```tsx title="src/admin/routes/custom/page.tsx"
+const navigate = useNavigate()
+
+const table = useDataTable({
+ columns,
+ data: data?.products || [],
+ getRowId: (row) => row.id,
+ rowCount: data?.count || 0,
+ isLoading,
+ pagination: {
+ state: pagination,
+ onPaginationChange: setPagination,
+ },
+ search: {
+ state: search,
+ onSearchChange: setSearch,
+ },
+ filtering: {
+ state: filtering,
+ onFilteringChange: setFiltering,
+ },
+ filters,
+ sorting: {
+ // Pass the pagination state and updater to the table instance
+ state: sorting,
+ onSortingChange: setSorting,
+ },
+ onRowClick: (event, row) => {
+ // Handle row click, for example
+ navigate(`/products/${row.id}`)
+ },
+})
+
+// TODO render component
+```
+
+The `useDataTable` hook accepts an object with the following properties:
+
+- columns: (\`array\`) The columns to display in the data table. You created this using the \`createDataTableColumnHelper\` utility.
+- data: (\`array\`) The products fetched from the Medusa application.
+- getRowId: (\`function\`) A function that returns the unique ID of a row.
+- rowCount: (\`number\`) The total number of products that can be retrieved. This is used to determine the number of pages.
+- isLoading: (\`boolean\`) A boolean that indicates if the data is being fetched.
+- pagination: (\`object\`) An object to configure pagination.
+
+ - state: (\`object\`) The pagination React state variable.
+
+ - onPaginationChange: (\`function\`) A function that updates the pagination state.
+- search: (\`object\`) An object to configure searching.
+
+ - state: (\`string\`) The search query React state variable.
+
+ - onSearchChange: (\`function\`) A function that updates the search query state.
+- filtering: (\`object\`) An object to configure filtering.
+
+ - state: (\`object\`) The filtering React state variable.
+
+ - onFilteringChange: (\`function\`) A function that updates the filtering state.
+- filters: (\`array\`) The filters to display in the data table. You created this using the \`createDataTableFilterHelper\` utility.
+- sorting: (\`object\`) An object to configure sorting.
+
+ - state: (\`object\`) The sorting React state variable.
+
+ - onSortingChange: (\`function\`) A function that updates the sorting state.
+- onRowClick: (\`function\`) A function that allows you to perform an action when the user clicks on a row. In this example, you navigate to the product's detail page.
+
+ - event: (\`mouseevent\`) An instance of the \[MouseClickEvent]\(https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent) object.
+
+ - row: (\`object\`) The data of the row that was clicked.
+
+Finally, you'll render the data table. But first, add the following imports at the top of the page:
+
+```tsx title="src/admin/routes/custom/page.tsx"
+import {
+ // ...
+ DataTable,
+} from "@medusajs/ui"
+import { SingleColumnLayout } from "../../layouts/single-column"
+import { Container } from "../../components/container"
+```
+
+Aside from the `DataTable` component, you also import the [SingleColumnLayout](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/layouts/single-column/index.html.md) and [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) components implemented in other Admin Component guides. These components ensure a style consistent to other pages in the admin dashboard.
+
+Then, replace the `TODO` in the component with the following:
+
+```tsx title="src/admin/routes/custom/page.tsx"
+return (
+
+
+
+
+ Products
+
+
+
+
+
+
+
+
+
+
+
+)
+```
+
+You render the `DataTable` component and pass the `table` instance as a prop. In the `DataTable` component, you render a toolbar showing a heading, filter menu, sorting menu, and a search input. You also show pagination after the table.
+
+Lastly, export the component and the UI widget's configuration at the end of the file:
+
+```tsx title="src/admin/routes/custom/page.tsx"
+// other imports...
+import { defineRouteConfig } from "@medusajs/admin-sdk"
+import { ChatBubbleLeftRight } from "@medusajs/icons"
+
+// ...
+
+export const config = defineRouteConfig({
+ label: "Custom",
+ icon: ChatBubbleLeftRight,
+})
+
+export default CustomPage
+```
+
+If you start your Medusa application and go to `localhost:9000/app/custom`, you'll see the data table showing the list of products with pagination, filtering, searching, and sorting functionalities.
+
+### Full Example Code
+
+```tsx title="src/admin/routes/custom/page.tsx"
+import { defineRouteConfig } from "@medusajs/admin-sdk"
+import { ChatBubbleLeftRight } from "@medusajs/icons"
+import {
+ Badge,
+ createDataTableColumnHelper,
+ createDataTableFilterHelper,
+ DataTable,
+ DataTableFilteringState,
+ DataTablePaginationState,
+ DataTableSortingState,
+ Heading,
+ useDataTable,
+} from "@medusajs/ui"
+import { useQuery } from "@tanstack/react-query"
+import { SingleColumnLayout } from "../../layouts/single-column"
+import { sdk } from "../../lib/config"
+import { useMemo, useState } from "react"
+import { Container } from "../../components/container"
+import { HttpTypes, ProductStatus } from "@medusajs/framework/types"
+
+const columnHelper = createDataTableColumnHelper()
+
+const columns = [
+ columnHelper.accessor("title", {
+ header: "Title",
+ // Enables sorting for the column.
+ enableSorting: true,
+ // If omitted, the header will be used instead if it's a string,
+ // otherwise the accessor key (id) will be used.
+ sortLabel: "Title",
+ // If omitted the default value will be "A-Z"
+ sortAscLabel: "A-Z",
+ // If omitted the default value will be "Z-A"
+ sortDescLabel: "Z-A",
+ }),
+ columnHelper.accessor("status", {
+ header: "Status",
+ cell: ({ getValue }) => {
+ const status = getValue()
+ return (
+
+ {status === "published" ? "Published" : "Draft"}
+
+ )
+ },
+ }),
+]
+
+const filterHelper = createDataTableFilterHelper()
+
+const filters = [
+ filterHelper.accessor("status", {
+ type: "select",
+ label: "Status",
+ options: [
+ {
+ label: "Published",
+ value: "published",
+ },
+ {
+ label: "Draft",
+ value: "draft",
+ },
+ ],
+ }),
+]
+
+const limit = 15
+
+const CustomPage = () => {
+ const [pagination, setPagination] = useState({
+ pageSize: limit,
+ pageIndex: 0,
+ })
+ const [search, setSearch] = useState("")
+ const [filtering, setFiltering] = useState({})
+ const [sorting, setSorting] = useState(null)
+
+ const offset = useMemo(() => {
+ return pagination.pageIndex * limit
+ }, [pagination])
+ const statusFilters = useMemo(() => {
+ return (filtering.status || []) as ProductStatus
+ }, [filtering])
+
+ const { data, isLoading } = useQuery({
+ queryFn: () => sdk.admin.product.list({
+ limit,
+ offset,
+ q: search,
+ status: statusFilters,
+ order: sorting ? `${sorting.desc ? "-" : ""}${sorting.id}` : undefined,
+ }),
+ queryKey: [["products", limit, offset, search, statusFilters, sorting?.id, sorting?.desc]],
+ })
+
+ const table = useDataTable({
+ columns,
+ data: data?.products || [],
+ getRowId: (row) => row.id,
+ rowCount: data?.count || 0,
+ isLoading,
+ pagination: {
+ state: pagination,
+ onPaginationChange: setPagination,
+ },
+ search: {
+ state: search,
+ onSearchChange: setSearch,
+ },
+ filtering: {
+ state: filtering,
+ onFilteringChange: setFiltering,
+ },
+ filters,
+ sorting: {
+ // Pass the pagination state and updater to the table instance
+ state: sorting,
+ onSortingChange: setSorting,
+ },
+ })
+
+ return (
+
+
+
+
+ Products
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export const config = defineRouteConfig({
+ label: "Custom",
+ icon: ChatBubbleLeftRight,
+})
+
+export default CustomPage
+```
+
+
# 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.
@@ -67898,98 +68021,6 @@ 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.
-
-
-
-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 (
-
- )
-}
-```
-
-The `SectionRow` component shows a title and a value in the same row.
-
-It accepts the following props:
-
-- title: (\`string\`) The title to show on the left side.
-- value: (\`React.ReactNode\` \\| \`string\` \\| \`null\`) The value to show on the right side.
-- actions: (\`React.ReactNode\`) The actions to show at the end of the row.
-
-***
-
-## Example
-
-Use the `SectionRow` component in any widget or UI route.
-
-For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content:
-
-```tsx title="src/admin/widgets/product-widget.tsx"
-import { defineWidgetConfig } from "@medusajs/admin-sdk"
-import { Container } from "../components/container"
-import { Header } from "../components/header"
-import { SectionRow } from "../components/section-row"
-
-const ProductWidget = () => {
- return (
-
-
-
-
- )
-}
-
-export const config = defineWidgetConfig({
- zone: "product.details.before",
-})
-
-export default ProductWidget
-```
-
-This widget also uses the [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) and [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) custom component.
-
-
# Single Column Layout - Admin Components
The Medusa Admin has pages with a single column of content.
@@ -68194,6 +68225,46 @@ const posts = await postModuleService.createPosts([
If an array is passed of the method, an array of the created records is also returned.
+# delete Method - Service Factory Reference
+
+This method deletes one or more records.
+
+## Delete One Record
+
+```ts
+await postModuleService.deletePosts("123")
+```
+
+To delete one record, pass its ID as a parameter of the method.
+
+***
+
+## Delete Multiple Records
+
+```ts
+await postModuleService.deletePosts([
+ "123",
+ "321",
+])
+```
+
+To delete multiple records, pass an array of IDs as a parameter of the method.
+
+***
+
+## Delete Records Matching Filters
+
+```ts
+await postModuleService.deletePosts({
+ name: "My Post",
+})
+```
+
+To delete records matching a set of filters, pass an object of filters as a parameter.
+
+Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md).
+
+
# listAndCount Method - Service Factory Reference
This method retrieves a list of records with the total count.
@@ -68330,46 +68401,6 @@ The method returns an array with two items:
2. The second is the total count of records.
-# delete Method - Service Factory Reference
-
-This method deletes one or more records.
-
-## Delete One Record
-
-```ts
-await postModuleService.deletePosts("123")
-```
-
-To delete one record, pass its ID as a parameter of the method.
-
-***
-
-## Delete Multiple Records
-
-```ts
-await postModuleService.deletePosts([
- "123",
- "321",
-])
-```
-
-To delete multiple records, pass an array of IDs as a parameter of the method.
-
-***
-
-## Delete Records Matching Filters
-
-```ts
-await postModuleService.deletePosts({
- name: "My Post",
-})
-```
-
-To delete records matching a set of filters, pass an object of filters as a parameter.
-
-Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md).
-
-
# list Method - Service Factory Reference
This method retrieves a list of records.
@@ -68488,6 +68519,63 @@ To sort records by one or more properties, pass to the second object parameter t
The method returns an array of the first `15` records matching the filters.
+# retrieve Method - Service Factory Reference
+
+This method retrieves one record of the data model by its ID.
+
+## Retrieve a Record
+
+```ts
+const post = await postModuleService.retrievePost("123")
+```
+
+### Parameters
+
+Pass the ID of the record to retrieve.
+
+### Returns
+
+The method returns the record as an object.
+
+***
+
+## Retrieve a Record's Relations
+
+This applies to relations between data models of the same module. To retrieve linked records of different modules, use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md).
+
+```ts
+const post = await postModuleService.retrievePost("123", {
+ relations: ["author"],
+})
+```
+
+### Parameters
+
+To retrieve the data model with relations, pass as a second parameter of the method an object with the property `relations`. `relations`'s value is an array of relation names.
+
+### Returns
+
+The method returns the record as an object.
+
+***
+
+## Select Properties to Retrieve
+
+```ts
+const post = await postModuleService.retrievePost("123", {
+ select: ["id", "name"],
+})
+```
+
+### Parameters
+
+By default, all of the record's properties are retrieved. To select specific ones, pass in the second object parameter a `select` property. Its value is an array of property names.
+
+### Returns
+
+The method returns the record as an object.
+
+
# softDelete Method - Service Factory Reference
This method soft deletes one or more records of the data model.
@@ -68575,93 +68663,6 @@ deletedPosts = {
```
-# restore Method - Service Factory Reference
-
-This method restores one or more records of the data model that were [soft-deleted](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/methods/soft-delete/index.html.md).
-
-## Restore One Record
-
-```ts
-const restoredPosts = await postModuleService.restorePosts("123")
-```
-
-### Parameters
-
-To restore one record, pass its ID as a parameter of the method.
-
-### Returns
-
-The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of restored records' IDs.
-
-For example, the returned object of the above example is:
-
-```ts
-restoredPosts = {
- post_id: ["123"],
-}
-```
-
-***
-
-## Restore Multiple Records
-
-```ts
-const restoredPosts = await postModuleService.restorePosts([
- "123",
- "321",
-])
-```
-
-### Parameters
-
-To restore multiple records, pass an array of IDs as a parameter of the method.
-
-### Returns
-
-The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of restored records' IDs.
-
-For example, the returned object of the above example is:
-
-```ts
-restoredPosts = {
- post_id: [
- "123",
- "321",
- ],
-}
-```
-
-***
-
-## Restore Records Matching Filters
-
-```ts
-const restoredPosts = await postModuleService.restorePosts({
- name: "My Post",
-})
-```
-
-### Parameters
-
-To restore records matching a set of filters, pass an object of fitlers as a parameter of the method.
-
-Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md).
-
-### Returns
-
-The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of restored records' IDs.
-
-For example, the returned object of the above example is:
-
-```ts
-restoredPosts = {
- post_id: [
- "123",
- ],
-}
-```
-
-
# update Method - Service Factory Reference
This method updates one or more records of the data model.
@@ -68785,62 +68786,92 @@ Learn more about accepted filters in [this documentation](https://docs.medusajs.
The method returns an array of objects of updated records.
-# retrieve Method - Service Factory Reference
+# restore Method - Service Factory Reference
-This method retrieves one record of the data model by its ID.
+This method restores one or more records of the data model that were [soft-deleted](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/methods/soft-delete/index.html.md).
-## Retrieve a Record
+## Restore One Record
```ts
-const post = await postModuleService.retrievePost("123")
+const restoredPosts = await postModuleService.restorePosts("123")
```
### Parameters
-Pass the ID of the record to retrieve.
+To restore one record, pass its ID as a parameter of the method.
### Returns
-The method returns the record as an object.
+The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of restored records' IDs.
+
+For example, the returned object of the above example is:
+
+```ts
+restoredPosts = {
+ post_id: ["123"],
+}
+```
***
-## Retrieve a Record's Relations
-
-This applies to relations between data models of the same module. To retrieve linked records of different modules, use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md).
+## Restore Multiple Records
```ts
-const post = await postModuleService.retrievePost("123", {
- relations: ["author"],
+const restoredPosts = await postModuleService.restorePosts([
+ "123",
+ "321",
+])
+```
+
+### Parameters
+
+To restore multiple records, pass an array of IDs as a parameter of the method.
+
+### Returns
+
+The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of restored records' IDs.
+
+For example, the returned object of the above example is:
+
+```ts
+restoredPosts = {
+ post_id: [
+ "123",
+ "321",
+ ],
+}
+```
+
+***
+
+## Restore Records Matching Filters
+
+```ts
+const restoredPosts = await postModuleService.restorePosts({
+ name: "My Post",
})
```
### Parameters
-To retrieve the data model with relations, pass as a second parameter of the method an object with the property `relations`. `relations`'s value is an array of relation names.
+To restore records matching a set of filters, pass an object of fitlers as a parameter of the method.
+
+Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md).
### Returns
-The method returns the record as an object.
+The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of restored records' IDs.
-***
-
-## Select Properties to Retrieve
+For example, the returned object of the above example is:
```ts
-const post = await postModuleService.retrievePost("123", {
- select: ["id", "name"],
-})
+restoredPosts = {
+ post_id: [
+ "123",
+ ],
+}
```
-### Parameters
-
-By default, all of the record's properties are retrieved. To select specific ones, pass in the second object parameter a `select` property. Its value is an array of property names.
-
-### Returns
-
-The method returns the record as an object.
-
# Filter Records - Service Factory Reference