diff --git a/www/apps/book/app/learn/fundamentals/modules/container/page.mdx b/www/apps/book/app/learn/fundamentals/modules/container/page.mdx
index 2e23083b36..b6daa783b7 100644
--- a/www/apps/book/app/learn/fundamentals/modules/container/page.mdx
+++ b/www/apps/book/app/learn/fundamentals/modules/container/page.mdx
@@ -6,9 +6,9 @@ export const metadata = {
In this chapter, you'll learn about the module's container and how to resolve resources in that container.
-Since modules are isolated, each module has a local container only used by the resources of that module.
+Since modules are [isolated](../isolation/page.mdx), each module has a local container only used by the resources of that module.
-So, resources in the module, such as services or loaders, can only resolve other resources registered in the module's container.
+So, resources in the module, such as services or loaders, can only resolve other resources registered in the module's container, and some Framework tools that the Medusa application registers in the module's container.
### List of Registered Resources
diff --git a/www/apps/book/app/learn/fundamentals/modules/isolation/page.mdx b/www/apps/book/app/learn/fundamentals/modules/isolation/page.mdx
index 1e541aa691..e718d08d5a 100644
--- a/www/apps/book/app/learn/fundamentals/modules/isolation/page.mdx
+++ b/www/apps/book/app/learn/fundamentals/modules/isolation/page.mdx
@@ -9,7 +9,8 @@ In this chapter, you'll learn how modules are isolated, and what that means for
- Modules can't access resources, such as services or data models, from other modules.
-- Use Medusa's linking concepts, as explained in the [Module Links chapters](../../../fundamentals/module-links/page.mdx), to extend a module's data models and retrieve data across modules.
+- Use [Module Links](../../../fundamentals/module-links/page.mdx) to extend an existing module's data models, and [Query](../../module-links/query/page.mdx) to retrieve data across modules.
+- Use [workflows](../../workflows/page.mdx) to build features that depend on functionalities from different modules.
@@ -19,6 +20,14 @@ A module is unaware of any resources other than its own, such as services or dat
For example, your custom module can't resolve the Product Module's main service or have direct relationships from its data model to the Product Module's data models.
+A module has its own container, as explained in the [Module Container](../container/page.mdx) chapter. This container includes the module's resources, such as services and data models, and some Framework resources that the Medusa application provides.
+
+
+
+Refer to the [Module Container Resources](!resources!/medusa-container-resources) for a list of resources registered in a module's container.
+
+
+
---
## Why are Modules Isolated
@@ -26,20 +35,24 @@ For example, your custom module can't resolve the Product Module's main service
Some of the module isolation's benefits include:
- Integrate your module into any Medusa application without side-effects to your setup.
-- Replace existing modules with your custom implementation, if your use case is drastically different.
+- Replace existing modules with your custom implementation if your use case is drastically different.
- Use modules in other environments, such as Edge functions and Next.js apps.
---
## How to Extend Data Model of Another Module?
-To extend the data model of another module, such as the `product` data model of the Product Module, use Medusa's linking concepts as explained in the [Module Links chapters](../../../fundamentals/module-links/page.mdx).
+To extend the data model of another module, such as the `Product` data model of the Product Module, use [Module Links](../../../fundamentals/module-links/page.mdx). Module Links allow you to build associations between data models of different modules without breaking the module isolation.
+
+Then, you can retrieve data across modules using [Query](../../module-links/query/page.mdx).
---
## How to Use Services of Other Modules?
-If you're building a feature that uses functionalities from different modules, use a workflow whose steps resolve the modules' services to perform these functionalities.
+You'll often build feature that uses functionalities from different modules. For example, if you may need to retrieve brands, then sync them to a third-party service.
+
+To build functionalities spanning across modules and systems, create a [workflow](../../workflows/page.mdx) whose steps resolve the modules' services to perform these functionalities.
Workflows ensure data consistency through their roll-back mechanism and tracking of each execution's status, steps, input, and output.
@@ -63,7 +76,7 @@ const retrieveBrandsStep = createStep(
"retrieve-brands",
async (_, { container }) => {
const brandModuleService = container.resolve(
- "brandModuleService"
+ "brand"
)
const brands = await brandModuleService.listBrands()
@@ -76,7 +89,7 @@ const createBrandsInCmsStep = createStep(
"create-brands-in-cms",
async ({ brands }, { container }) => {
const cmsModuleService = container.resolve(
- "cmsModuleService"
+ "cms"
)
const cmsBrands = await cmsModuleService.createBrands(brands)
@@ -85,7 +98,7 @@ const createBrandsInCmsStep = createStep(
},
async (brands, { container }) => {
const cmsModuleService = container.resolve(
- "cmsModuleService"
+ "cms"
)
await cmsModuleService.deleteBrands(
@@ -95,7 +108,7 @@ const createBrandsInCmsStep = createStep(
)
```
-The `retrieveBrandsStep` retrieves the brands from a brand module, and the `createBrandsInCmsStep` creates the brands in a third-party system using a CMS module.
+The `retrieveBrandsStep` retrieves the brands from a Brand Module, and the `createBrandsInCmsStep` creates the brands in a third-party system using a CMS Module.
Then, create the following workflow that uses these steps:
@@ -111,3 +124,154 @@ export const syncBrandsWorkflow = createWorkflow(
```
You can then use this workflow in an API route, scheduled job, or other resources that use this functionality.
+
+---
+
+## How to Use Framework APIs and Tools in Module?
+
+### Framework Tools in Module Container
+
+A module has in its container some Framework APIs and tools, such as [Logger](../../../debugging-and-testing/logging/page.mdx). You can refer to the [Module Container Resources](!resources!/medusa-container-resources) for a list of resources registered in a module's container.
+
+You can resolve those resources in the module's services and loaders.
+
+For example:
+
+```ts title="Example Service"
+import { Logger } from "@medusajs/framework/types"
+
+type InjectedDependencies = {
+ logger: Logger
+}
+
+export default class BlogModuleService {
+ protected logger_: Logger
+
+ constructor({ logger }: InjectedDependencies) {
+ this.logger_ = logger
+
+ this.logger_.info("[BlogModuleService]: Hello World!")
+ }
+
+ // ...
+}
+```
+
+In this example, the `BlogModuleService` class resolves the `Logger` service from the module's container and uses it to log a message.
+
+### Using Framework Tools in Workflows
+
+Some Framework APIs and tools are not registered in the module's container. For example, [Query](../../module-links/query/page.mdx) is only registered in the Medusa container.
+
+You should, instead, build workflows that use these APIs and tools along with your module's service.
+
+For example, you can create a workflow that retrieves data using Query, then pass the data to your module's service to perform some action.
+
+```ts title="Example Workflow"
+import { createWorkflow, createStep } from "@medusajs/framework/workflows-sdk"
+import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
+
+const createBrandsInCmsStep = createStep(
+ "create-brands-in-cms",
+ async ({ brands }, { container }) => {
+ const cmsModuleService = container.resolve(
+ "cms"
+ )
+
+ const cmsBrands = await cmsModuleService.createBrands(brands)
+
+ return new StepResponse(cmsBrands, cmsBrands)
+ },
+ async (brands, { container }) => {
+ const cmsModuleService = container.resolve(
+ "cms"
+ )
+
+ await cmsModuleService.deleteBrands(
+ brands.map((brand) => brand.id)
+ )
+ }
+)
+
+const syncBrandsWorkflow = createWorkflow(
+ "sync-brands",
+ () => {
+ const { data: brands } = useQueryGraphStep({
+ entity: "brand",
+ fields: [
+ "*",
+ "products.*",
+ ],
+ })
+
+ createBrandsInCmsStep({ brands })
+ }
+)
+```
+
+In this example, you use the `useQueryGraphStep` to retrieve brands with their products, then pass the brands to the `createBrandsInCmsStep` step.
+
+In the `createBrandsInCmsStep`, you resolve the CMS Module's service from the module's container and use it to create the brands in the third-party system. You pass the brands you retrieved using Query to the module's service.
+
+### Injecting Dependencies to Module
+
+Some cases still require you to access external resources, mainly [Infrastructure Modules](!resources!/infrastructure-modules) or Framework tools, in your module.
+For example, you may need the [Event Module](!resources!/infrastructure-modules/event) to emit events from your module's service.
+
+In those cases, you can inject the dependencies to your module's service in `medusa-config.ts` using the `dependencies` property of the module's configuration.
+
+
+
+Use this approach only when absolutely necessary, where workflows aren't sufficient for your use case. By injecting dependencies, you risk breaking your module if the dependency isn't provided, or if the dependency's API changes.
+
+
+
+For example:
+
+```ts title="medusa-config.ts"
+import { Modules } from "@medusajs/framework/utils"
+
+module.exports = defineConfig({
+ // ...
+ modules: [
+ {
+ resolve: "./src/modules/blog",
+ dependencies: [
+ Modules.EVENT_BUS,
+ ],
+ },
+ ],
+})
+```
+
+In this example, you inject the Event Module's service to your module's container.
+
+
+
+Only the main service will be injected into the module's container.
+
+
+
+You can then use the Event Module's service in your module's service:
+
+```ts title="Example Service"
+class BlogModuleService {
+ protected eventBusService_: AbstractEventBusModuleService
+
+ constructor({ event_bus }) {
+ this.eventBusService_ = event_bus
+ }
+
+ performAction() {
+ // TODO perform action
+
+ this.eventBusService_.emit({
+ name: "custom.event",
+ data: {
+ id: "123",
+ // other data payload
+ },
+ })
+ }
+}
+```
\ No newline at end of file
diff --git a/www/apps/book/app/learn/fundamentals/modules/loaders/page.mdx b/www/apps/book/app/learn/fundamentals/modules/loaders/page.mdx
index 361fd4b4c4..265bb1e0fc 100644
--- a/www/apps/book/app/learn/fundamentals/modules/loaders/page.mdx
+++ b/www/apps/book/app/learn/fundamentals/modules/loaders/page.mdx
@@ -102,12 +102,18 @@ This indicates that the loader in the `hello` module ran and logged this message
## When are Loaders Executed?
+### Loaders Executed on Application Startup
+
When you start the Medusa application, it executes the loaders of all modules in their registration order.
A loader is executed before the module's main service is instantiated. So, you can use loaders to register in the module's container resources that you want to use in the module's service. For example, you can register a database connection.
Loaders are also useful to only load a module if a certain condition is met. For example, if you try to connect to a database in a loader but the connection fails, you can throw an error in the loader to prevent the module from being loaded. This is useful if your module depends on an external service to work.
+### Loaders Executed with Migrations
+
+Loaders are also executed when you run [migrations](../../data-models/write-migration/page.mdx). This can be useful if you need to run some task before the migrations, or you want to migrate some data to an integrated third-party system as part of the migration process.
+
---
## Example: Register Custom MongoDB Connection
diff --git a/www/apps/book/generated/edit-dates.mjs b/www/apps/book/generated/edit-dates.mjs
index 21eb03def1..0844c41276 100644
--- a/www/apps/book/generated/edit-dates.mjs
+++ b/www/apps/book/generated/edit-dates.mjs
@@ -16,9 +16,9 @@ export const generatedEditDates = {
"app/learn/fundamentals/api-routes/page.mdx": "2024-12-04T11:02:57.134Z",
"app/learn/fundamentals/modules/modules-directory-structure/page.mdx": "2024-12-09T10:32:46.839Z",
"app/learn/fundamentals/events-and-subscribers/page.mdx": "2025-05-16T13:40:16.111Z",
- "app/learn/fundamentals/modules/container/page.mdx": "2025-03-18T15:10:03.574Z",
+ "app/learn/fundamentals/modules/container/page.mdx": "2025-05-21T15:07:12.059Z",
"app/learn/fundamentals/workflows/execute-another-workflow/page.mdx": "2024-12-09T15:56:22.895Z",
- "app/learn/fundamentals/modules/loaders/page.mdx": "2025-04-22T15:32:00.430Z",
+ "app/learn/fundamentals/modules/loaders/page.mdx": "2025-05-21T15:15:35.271Z",
"app/learn/fundamentals/admin/widgets/page.mdx": "2024-12-09T16:43:24.260Z",
"app/learn/fundamentals/data-models/page.mdx": "2025-03-18T07:55:56.252Z",
"app/learn/fundamentals/modules/remote-link/page.mdx": "2024-09-30T08:43:53.127Z",
@@ -47,7 +47,7 @@ export const generatedEditDates = {
"app/learn/fundamentals/api-routes/cors/page.mdx": "2025-03-11T08:54:26.281Z",
"app/learn/fundamentals/admin/ui-routes/page.mdx": "2025-02-24T09:35:11.752Z",
"app/learn/fundamentals/api-routes/middlewares/page.mdx": "2025-05-09T07:56:04.125Z",
- "app/learn/fundamentals/modules/isolation/page.mdx": "2024-12-09T11:02:38.087Z",
+ "app/learn/fundamentals/modules/isolation/page.mdx": "2025-05-21T15:10:15.499Z",
"app/learn/fundamentals/data-models/index/page.mdx": "2025-03-18T07:59:07.798Z",
"app/learn/fundamentals/custom-cli-scripts/page.mdx": "2024-10-23T07:08:55.898Z",
"app/learn/debugging-and-testing/testing-tools/integration-tests/api-routes/page.mdx": "2025-03-18T15:06:27.864Z",
diff --git a/www/apps/book/public/llms-full.txt b/www/apps/book/public/llms-full.txt
index 13e7386e11..0060181f50 100644
--- a/www/apps/book/public/llms-full.txt
+++ b/www/apps/book/public/llms-full.txt
@@ -473,135 +473,6 @@ npm install
```
-# 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).
-
-
# Medusa Application Configuration
In this chapter, you'll learn available configurations in the Medusa application. You can change the application's configurations to customize the behavior of the application, its integrated modules and plugins, and more.
@@ -2177,7 +2048,7 @@ The Medusa application's production build, which is created using the `build` co
If your hosting provider doesn't support setting a current-working directory, set the start command to the following:
```bash npm2yarn
-cd .medusa/server && npm run install && npm run start
+cd .medusa/server && npm install && npm run start
```
***
@@ -2203,6 +2074,208 @@ Replace the email `admin-medusa@test.com` and password `supersecret` with the cr
You can use these credentials to log into the Medusa Admin dashboard.
+# 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.
+
+
+# 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).
+
+
+# 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.
+
+
+# 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:
+
+- [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.
@@ -2378,291 +2451,6 @@ curl http://localhost:9000/hello-world
You're exposing custom functionality to be used by a storefront, admin dashboard, or any external application.
-# Environment Variables
-
-In this chapter, you'll learn how environment variables are loaded in Medusa.
-
-## System Environment Variables
-
-The Medusa application loads and uses system environment variables.
-
-For example, if you set the `PORT` environment variable to `8000`, the Medusa application runs on that port instead of `9000`.
-
-In production, you should always use system environment variables that you set through your hosting provider.
-
-***
-
-## Environment Variables in .env Files
-
-During development, it's easier to set environment variables in a `.env` file in your repository.
-
-Based on your `NODE_ENV` system environment variable, Medusa will try to load environment variables from the following `.env` files:
-
-As of [Medusa v2.5.0](https://github.com/medusajs/medusa/releases/tag/v2.5.0), `NODE_ENV` defaults to `production` when using `medusa start`. Otherwise, it defaults to `development`.
-
-|\`.env\`|
-|---|---|
-|\`NODE\_ENV\`|\`.env\`|
-|\`NODE\_ENV\`|\`.env.production\`|
-|\`NODE\_ENV\`|\`.env.staging\`|
-|\`NODE\_ENV\`|\`.env.test\`|
-
-### Set Environment in `loadEnv`
-
-In the `medusa-config.ts` file of your Medusa application, you'll find a `loadEnv` function used that accepts `process.env.NODE_ENV` as a first parameter.
-
-This function is responsible for loading the correct `.env` file based on the value of `process.env.NODE_ENV`.
-
-To ensure that the correct `.env` file is loaded as shown in the table above, only specify `development`, `production`, `staging` or `test` as the value of `process.env.NODE_ENV` or as the parameter of `loadEnv`.
-
-***
-
-## Environment Variables for Admin Customizations
-
-Since the Medusa Admin is built on top of [Vite](https://vite.dev/), you prefix the environment variables you want to use in a widget or UI route with `VITE_`. Then, you can access or use them with the `import.meta.env` object.
-
-Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/admin/environment-variables/index.html.md).
-
-***
-
-## Predefined Medusa Environment Variables
-
-The Medusa application uses the following predefined environment variables that you can set:
-
-You should opt for setting configurations in `medusa-config.ts` where possible. For a full list of Medusa configurations, refer to the [Medusa Configurations chapter](https://docs.medusajs.com/learn/configurations/medusa-config/index.html.md).
-
-|Environment Variable|Description|Default|
-|---|---|---|---|---|
-|
-|
-|
-|
-||The URL to connect to the PostgreSQL database. Only used if ||
-||URLs of storefronts that can access the Medusa backend's Store APIs. Only used if ||
-||URLs of admin dashboards that can access the Medusa backend's Admin APIs. Only used if ||
-||URLs of clients that can access the Medusa backend's authentication routes. Only used if ||
-||A random string used to create authentication tokens in the http layer. Only used if ||
-||A random string used to create cookie tokens in the http layer. Only used if ||
-||The URL to the Medusa backend. Only used if ||
-|
-|
-|
-|
-|
-|
-|
-|
-||The allowed levels to log. Learn more in ||
-||The file to save logs in. By default, logs aren't saved in any file. Learn more in ||
-||Whether to disable analytics data collection. Learn more in ||
-
-
-# Data Models
-
-In this chapter, you'll learn what a data model is and how to create a data model.
-
-## What is a Data Model?
-
-A data model represents a table in the database. You create data models using Medusa's data modeling language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations.
-
-You create a data model in a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). The module's service provides the methods to store and manage those data models. Then, you can resolve the module's service in other customizations, such as a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), to manage the data models' records.
-
-***
-
-## How to Create a Data Model
-
-In a module, you can create a data model in a TypeScript or JavaScript file under the module's `models` directory.
-
-So, for example, assuming you have a Blog Module at `src/modules/blog`, you can create a `Post` data model by creating the `src/modules/blog/models/post.ts` file with the following content:
-
-
-
-```ts title="src/modules/blog/models/post.ts"
-import { model } from "@medusajs/framework/utils"
-
-const Post = model.define("post", {
- id: model.id().primaryKey(),
- title: model.text(),
-})
-
-export default Post
-```
-
-You define the data model using the `define` method of the DML. It accepts two parameters:
-
-1. The first one is the name of the data model's table in the database. Use snake-case names.
-2. The second is an object, which is the data model's schema. The schema's properties are defined using the `model`'s methods, such as `text` and `id`.
- - Data models automatically have the date properties `created_at`, `updated_at`, and `deleted_at`, so you don't need to add them manually.
-
-The code snippet above defines a `Post` data model with `id` and `title` properties.
-
-***
-
-## Generate Migrations
-
-After you create a data model in a module, then [register that module in your Medusa configurations](https://docs.medusajs.com/learn/fundamentals/modules#4-add-module-to-medusas-configurations/index.html.md), you must generate a migration to create the data model's table in the database.
-
-A migration is a TypeScript or JavaScript file that defines database changes made by a module. Migrations are useful when you re-use a module or you're working in a team, so that when one member of a team makes a database change, everyone else can reflect it on their side by running the migrations.
-
-For example, to generate a migration for the Blog Module, run the following command in your Medusa application's directory:
-
-If you're creating the module in a plugin, use the [plugin:db:generate command](https://docs.medusajs.com/resources/medusa-cli/commands/plugin#plugindbgenerate/index.html.md) instead.
-
-```bash
-npx medusa db:generate blog
-```
-
-The `db:generate` command of the Medusa CLI accepts one or more module names to generate the migration for. It will create a migration file for the Blog Module in the directory `src/modules/blog/migrations` similar to the following:
-
-```ts
-import { Migration } from "@mikro-orm/migrations"
-
-export class Migration20241121103722 extends Migration {
-
- async up(): Promise {
- this.addSql("create table if not exists \"post\" (\"id\" text not null, \"title\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"post_pkey\" primary key (\"id\"));")
- }
-
- async down(): Promise {
- this.addSql("drop table if exists \"post\" cascade;")
- }
-
-}
-```
-
-In the migration class, the `up` method creates the table `post` and defines its columns using PostgreSQL syntax. The `down` method drops the table.
-
-### Run Migrations
-
-To reflect the changes in the generated migration file on the database, run the `db:migrate` command:
-
-If you're creating the module in a plugin, run this command on the Medusa application that the plugin is installed in.
-
-```bash
-npx medusa db:migrate
-```
-
-This creates the `post` table in the database.
-
-### Migrations on Data Model Changes
-
-Whenever you make a change to a data model, you must generate and run the migrations.
-
-For example, if you add a new column to the `Post` data model, you must generate a new migration and run it.
-
-***
-
-## Manage Data Models
-
-Your module's service should extend the [service factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md), which generates data-management methods for your module's data models.
-
-For example, the Blog Module's service would have methods like `retrievePost` and `createPosts`.
-
-Refer to the [Service Factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md) chapter to learn more about how to extend the service factory and manage data models, and refer to the [Service Factory Reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) for the full list of generated methods and how to use them.
-
-
-# Events and Subscribers
-
-In this chapter, you’ll learn about Medusa's event system, and how to handle events with subscribers.
-
-## Handle Core Commerce Flows with Events
-
-When building commerce digital applications, you'll often need to perform an action after a commerce operation is performed. For example, sending an order confirmation email when the customer places an order, or syncing data that's updated in Medusa to a third-party system.
-
-Medusa emits events when core commerce features are performed, and you can listen to and handle these events in asynchronous functions. You can think of Medusa's events like you'd think about webhooks in other commerce platforms, but instead of having to setup separate applications to handle webhooks, your efforts only go into writing the logic right in your Medusa codebase.
-
-You listen to an event in a subscriber, which is an asynchronous function that's executed when its associated event is emitted.
-
-
-
-Subscribers are useful to perform actions that aren't integral to the original flow. For example, you can handle the `order.placed` event in a subscriber that sends a confirmation email to the customer. The subscriber has no impact on the original order-placement flow, as it's executed outside of it.
-
-If the action you're performing is integral to the main flow of the core commerce feature, use [workflow hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md) instead.
-
-### List of Emitted Events
-
-Find a list of all emitted events in [this reference](https://docs.medusajs.com/resources/references/events/index.html.md).
-
-***
-
-## How to Create a Subscriber?
-
-You create a subscriber in a TypeScript or JavaScript file under the `src/subscribers` directory. The file exports the function to execute and the subscriber's configuration that indicate what event(s) it listens to.
-
-For example, create the file `src/subscribers/product-created.ts` with the following content:
-
-
-
-```ts title="src/subscribers/product-created.ts"
-import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
-import { sendOrderConfirmationWorkflow } from "../workflows/send-order-confirmation"
-
-export default async function orderPlacedHandler({
- event: { data },
- container,
-}: SubscriberArgs<{ id: string }>) {
- const logger = container.resolve("logger")
-
- logger.info("Sending confirmation email...")
-
- await sendOrderConfirmationWorkflow(container)
- .run({
- input: {
- id: data.id,
- },
- })
-}
-
-export const config: SubscriberConfig = {
- event: `order.placed`,
-}
-```
-
-This subscriber file exports:
-
-- An asynchronous subscriber function that's executed whenever the associated event, which is `order.placed` is triggered.
-- A configuration object with an `event` property whose value is the event the subscriber is listening to. You can also pass an array of event names to listen to multiple events in the same subscriber.
-
-The subscriber function receives an object as a parameter that has the following properties:
-
-- `event`: An object with the event's details. The `data` property contains the data payload of the event emitted, which is the order's ID in this case.
-- `container`: The [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) that you can use to resolve registered resources.
-
-In the subscriber function, you use the container to resolve the Logger utility and log a message in the console. Also, assuming you have a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) that sends an order confirmation email, you execute it in the subscriber.
-
-***
-
-## Test the Subscriber
-
-To test the subscriber, start the Medusa application:
-
-```bash npm2yarn
-npm run dev
-```
-
-Then, try placing an order either using Medusa's API routes or the [Next.js Starter Storefront](https://docs.medusajs.com/resources/nextjs-starter/index.html.md). You'll see the following message in the terminal:
-
-```bash
-info: Processing order.placed which has 1 subscribers
-Sending confirmation email...
-```
-
-The first message indicates that the `order.placed` event was emitted, and the second one is the message logged from the subscriber.
-
-***
-
-## Event Module
-
-The subscription and emitting of events is handled by an Event Module, an Infrastructure Module that implements the pub/sub functionalities of Medusa's event system.
-
-Medusa provides two Event Modules out of the box:
-
-- [Local Event Module](https://docs.medusajs.com/resources/infrastructure-modules/event/local/index.html.md), used by default. It's useful for development, as you don't need additional setup to use it.
-- [Redis Event Module](https://docs.medusajs.com/resources/infrastructure-modules/event/redis/index.html.md), which is useful in production. It uses [Redis](https://redis.io/) to implement Medusa's pub/sub events system.
-
-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).
-
-
# Framework Overview
In this chapter, you'll learn about the Medusa Framework and how it facilitates building customizations in your Medusa application.
@@ -3433,6 +3221,291 @@ To learn more about the different concepts useful for building plugins, check ou
- [Plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md)
+# Events and Subscribers
+
+In this chapter, you’ll learn about Medusa's event system, and how to handle events with subscribers.
+
+## Handle Core Commerce Flows with Events
+
+When building commerce digital applications, you'll often need to perform an action after a commerce operation is performed. For example, sending an order confirmation email when the customer places an order, or syncing data that's updated in Medusa to a third-party system.
+
+Medusa emits events when core commerce features are performed, and you can listen to and handle these events in asynchronous functions. You can think of Medusa's events like you'd think about webhooks in other commerce platforms, but instead of having to setup separate applications to handle webhooks, your efforts only go into writing the logic right in your Medusa codebase.
+
+You listen to an event in a subscriber, which is an asynchronous function that's executed when its associated event is emitted.
+
+
+
+Subscribers are useful to perform actions that aren't integral to the original flow. For example, you can handle the `order.placed` event in a subscriber that sends a confirmation email to the customer. The subscriber has no impact on the original order-placement flow, as it's executed outside of it.
+
+If the action you're performing is integral to the main flow of the core commerce feature, use [workflow hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md) instead.
+
+### List of Emitted Events
+
+Find a list of all emitted events in [this reference](https://docs.medusajs.com/resources/references/events/index.html.md).
+
+***
+
+## How to Create a Subscriber?
+
+You create a subscriber in a TypeScript or JavaScript file under the `src/subscribers` directory. The file exports the function to execute and the subscriber's configuration that indicate what event(s) it listens to.
+
+For example, create the file `src/subscribers/product-created.ts` with the following content:
+
+
+
+```ts title="src/subscribers/product-created.ts"
+import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
+import { sendOrderConfirmationWorkflow } from "../workflows/send-order-confirmation"
+
+export default async function orderPlacedHandler({
+ event: { data },
+ container,
+}: SubscriberArgs<{ id: string }>) {
+ const logger = container.resolve("logger")
+
+ logger.info("Sending confirmation email...")
+
+ await sendOrderConfirmationWorkflow(container)
+ .run({
+ input: {
+ id: data.id,
+ },
+ })
+}
+
+export const config: SubscriberConfig = {
+ event: `order.placed`,
+}
+```
+
+This subscriber file exports:
+
+- An asynchronous subscriber function that's executed whenever the associated event, which is `order.placed` is triggered.
+- A configuration object with an `event` property whose value is the event the subscriber is listening to. You can also pass an array of event names to listen to multiple events in the same subscriber.
+
+The subscriber function receives an object as a parameter that has the following properties:
+
+- `event`: An object with the event's details. The `data` property contains the data payload of the event emitted, which is the order's ID in this case.
+- `container`: The [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) that you can use to resolve registered resources.
+
+In the subscriber function, you use the container to resolve the Logger utility and log a message in the console. Also, assuming you have a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) that sends an order confirmation email, you execute it in the subscriber.
+
+***
+
+## Test the Subscriber
+
+To test the subscriber, start the Medusa application:
+
+```bash npm2yarn
+npm run dev
+```
+
+Then, try placing an order either using Medusa's API routes or the [Next.js Starter Storefront](https://docs.medusajs.com/resources/nextjs-starter/index.html.md). You'll see the following message in the terminal:
+
+```bash
+info: Processing order.placed which has 1 subscribers
+Sending confirmation email...
+```
+
+The first message indicates that the `order.placed` event was emitted, and the second one is the message logged from the subscriber.
+
+***
+
+## Event Module
+
+The subscription and emitting of events is handled by an Event Module, an Infrastructure Module that implements the pub/sub functionalities of Medusa's event system.
+
+Medusa provides two Event Modules out of the box:
+
+- [Local Event Module](https://docs.medusajs.com/resources/infrastructure-modules/event/local/index.html.md), used by default. It's useful for development, as you don't need additional setup to use it.
+- [Redis Event Module](https://docs.medusajs.com/resources/infrastructure-modules/event/redis/index.html.md), which is useful in production. It uses [Redis](https://redis.io/) to implement Medusa's pub/sub events system.
+
+Medusa's [architecture](https://docs.medusajs.com/learn/introduction/architecture/index.html.md) also allows you to build a custom Event Module that uses a different service or logic to implement the pub/sub system. Learn how to build an Event Module in [this guide](https://docs.medusajs.com/resources/infrastructure-modules/event/create/index.html.md).
+
+
+# Environment Variables
+
+In this chapter, you'll learn how environment variables are loaded in Medusa.
+
+## System Environment Variables
+
+The Medusa application loads and uses system environment variables.
+
+For example, if you set the `PORT` environment variable to `8000`, the Medusa application runs on that port instead of `9000`.
+
+In production, you should always use system environment variables that you set through your hosting provider.
+
+***
+
+## Environment Variables in .env Files
+
+During development, it's easier to set environment variables in a `.env` file in your repository.
+
+Based on your `NODE_ENV` system environment variable, Medusa will try to load environment variables from the following `.env` files:
+
+As of [Medusa v2.5.0](https://github.com/medusajs/medusa/releases/tag/v2.5.0), `NODE_ENV` defaults to `production` when using `medusa start`. Otherwise, it defaults to `development`.
+
+|\`.env\`|
+|---|---|
+|\`NODE\_ENV\`|\`.env\`|
+|\`NODE\_ENV\`|\`.env.production\`|
+|\`NODE\_ENV\`|\`.env.staging\`|
+|\`NODE\_ENV\`|\`.env.test\`|
+
+### Set Environment in `loadEnv`
+
+In the `medusa-config.ts` file of your Medusa application, you'll find a `loadEnv` function used that accepts `process.env.NODE_ENV` as a first parameter.
+
+This function is responsible for loading the correct `.env` file based on the value of `process.env.NODE_ENV`.
+
+To ensure that the correct `.env` file is loaded as shown in the table above, only specify `development`, `production`, `staging` or `test` as the value of `process.env.NODE_ENV` or as the parameter of `loadEnv`.
+
+***
+
+## Environment Variables for Admin Customizations
+
+Since the Medusa Admin is built on top of [Vite](https://vite.dev/), you prefix the environment variables you want to use in a widget or UI route with `VITE_`. Then, you can access or use them with the `import.meta.env` object.
+
+Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/admin/environment-variables/index.html.md).
+
+***
+
+## Predefined Medusa Environment Variables
+
+The Medusa application uses the following predefined environment variables that you can set:
+
+You should opt for setting configurations in `medusa-config.ts` where possible. For a full list of Medusa configurations, refer to the [Medusa Configurations chapter](https://docs.medusajs.com/learn/configurations/medusa-config/index.html.md).
+
+|Environment Variable|Description|Default|
+|---|---|---|---|---|
+|
+|
+|
+|
+||The URL to connect to the PostgreSQL database. Only used if ||
+||URLs of storefronts that can access the Medusa backend's Store APIs. Only used if ||
+||URLs of admin dashboards that can access the Medusa backend's Admin APIs. Only used if ||
+||URLs of clients that can access the Medusa backend's authentication routes. Only used if ||
+||A random string used to create authentication tokens in the http layer. Only used if ||
+||A random string used to create cookie tokens in the http layer. Only used if ||
+||The URL to the Medusa backend. Only used if ||
+|
+|
+|
+|
+|
+|
+|
+|
+||The allowed levels to log. Learn more in ||
+||The file to save logs in. By default, logs aren't saved in any file. Learn more in ||
+||Whether to disable analytics data collection. Learn more in ||
+
+
+# Data Models
+
+In this chapter, you'll learn what a data model is and how to create a data model.
+
+## What is a Data Model?
+
+A data model represents a table in the database. You create data models using Medusa's data modeling language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations.
+
+You create a data model in a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). The module's service provides the methods to store and manage those data models. Then, you can resolve the module's service in other customizations, such as a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), to manage the data models' records.
+
+***
+
+## How to Create a Data Model
+
+In a module, you can create a data model in a TypeScript or JavaScript file under the module's `models` directory.
+
+So, for example, assuming you have a Blog Module at `src/modules/blog`, you can create a `Post` data model by creating the `src/modules/blog/models/post.ts` file with the following content:
+
+
+
+```ts title="src/modules/blog/models/post.ts"
+import { model } from "@medusajs/framework/utils"
+
+const Post = model.define("post", {
+ id: model.id().primaryKey(),
+ title: model.text(),
+})
+
+export default Post
+```
+
+You define the data model using the `define` method of the DML. It accepts two parameters:
+
+1. The first one is the name of the data model's table in the database. Use snake-case names.
+2. The second is an object, which is the data model's schema. The schema's properties are defined using the `model`'s methods, such as `text` and `id`.
+ - Data models automatically have the date properties `created_at`, `updated_at`, and `deleted_at`, so you don't need to add them manually.
+
+The code snippet above defines a `Post` data model with `id` and `title` properties.
+
+***
+
+## Generate Migrations
+
+After you create a data model in a module, then [register that module in your Medusa configurations](https://docs.medusajs.com/learn/fundamentals/modules#4-add-module-to-medusas-configurations/index.html.md), you must generate a migration to create the data model's table in the database.
+
+A migration is a TypeScript or JavaScript file that defines database changes made by a module. Migrations are useful when you re-use a module or you're working in a team, so that when one member of a team makes a database change, everyone else can reflect it on their side by running the migrations.
+
+For example, to generate a migration for the Blog Module, run the following command in your Medusa application's directory:
+
+If you're creating the module in a plugin, use the [plugin:db:generate command](https://docs.medusajs.com/resources/medusa-cli/commands/plugin#plugindbgenerate/index.html.md) instead.
+
+```bash
+npx medusa db:generate blog
+```
+
+The `db:generate` command of the Medusa CLI accepts one or more module names to generate the migration for. It will create a migration file for the Blog Module in the directory `src/modules/blog/migrations` similar to the following:
+
+```ts
+import { Migration } from "@mikro-orm/migrations"
+
+export class Migration20241121103722 extends Migration {
+
+ async up(): Promise {
+ this.addSql("create table if not exists \"post\" (\"id\" text not null, \"title\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"post_pkey\" primary key (\"id\"));")
+ }
+
+ async down(): Promise {
+ this.addSql("drop table if exists \"post\" cascade;")
+ }
+
+}
+```
+
+In the migration class, the `up` method creates the table `post` and defines its columns using PostgreSQL syntax. The `down` method drops the table.
+
+### Run Migrations
+
+To reflect the changes in the generated migration file on the database, run the `db:migrate` command:
+
+If you're creating the module in a plugin, run this command on the Medusa application that the plugin is installed in.
+
+```bash
+npx medusa db:migrate
+```
+
+This creates the `post` table in the database.
+
+### Migrations on Data Model Changes
+
+Whenever you make a change to a data model, you must generate and run the migrations.
+
+For example, if you add a new column to the `Post` data model, you must generate a new migration and run it.
+
+***
+
+## Manage Data Models
+
+Your module's service should extend the [service factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md), which generates data-management methods for your module's data models.
+
+For example, the Blog Module's service would have methods like `retrievePost` and `createPosts`.
+
+Refer to the [Service Factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md) chapter to learn more about how to extend the service factory and manage data models, and refer to the [Service Factory Reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) for the full list of generated methods and how to use them.
+
+
# Medusa Container
In this chapter, you’ll learn about the Medusa container and how to use it.
@@ -4098,44 +4171,6 @@ This will create a post and return it in the response:
You can also execute the workflow from a [subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) when an event occurs, or from a [scheduled job](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) to run it at a specified interval.
-# Plugins
-
-In this chapter, you'll learn what a plugin is in Medusa.
-
-Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0).
-
-## What is a Plugin?
-
-A plugin is a package of reusable Medusa customizations that you can install in any Medusa application. The supported customizations are [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), [API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md), [Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), [Workflow Hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md), [Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md), [Subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md), [Scheduled Jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md), and [Admin Extensions](https://docs.medusajs.com/learn/fundamentals/admin/index.html.md).
-
-Plugins allow you to reuse your Medusa customizations across multiple projects or share them with the community. They can be published to npm and installed in any Medusa project.
-
-
-
-Learn how to create a wishlist plugin in [this guide](https://docs.medusajs.com/resources/plugins/guides/wishlist/index.html.md).
-
-***
-
-## Plugin vs Module
-
-A [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) is an isolated package related to a single domain or functionality, such as product reviews or integrating a Content Management System. A module can't access any resources in the Medusa application that are outside its codebase.
-
-A plugin, on the other hand, can contain multiple Medusa customizations, including modules. Your plugin can define a module, then build flows around it.
-
-For example, in a plugin, you can define a module that integrates a third-party service, then add a workflow that uses the module when a certain event occurs to sync data to that service.
-
-- You want to reuse your Medusa customizations across multiple projects.
-- You want to share your Medusa customizations with the community.
-
-- You want to build a custom feature related to a single domain or integrate a third-party service. Instead, use a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). You can wrap that module in a plugin if it's used in other customizations, such as if it has a module link or it's used in a workflow.
-
-***
-
-## How to Create a Plugin?
-
-The next chapter explains how you can create and publish a plugin.
-
-
# Scheduled Jobs
In this chapter, you’ll learn about scheduled jobs and how to use them.
@@ -4230,77 +4265,134 @@ 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.
-# Medusa's Architecture
+# Worker Mode of Medusa Instance
-In this chapter, you'll learn about the architectural layers in Medusa.
+In this chapter, you'll learn about the different modes of running a Medusa instance and how to configure the mode.
-Find the full architectural diagram at the [end of this chapter](#full-diagram-of-medusas-architecture).
+## What is Worker Mode?
-## HTTP, Workflow, and Module Layers
+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.
-Medusa is a headless commerce platform. So, storefronts, admin dashboards, and other clients consume Medusa's functionalities through its API routes.
+In a production environment, you should deploy two separate instances of your Medusa application:
-In a common Medusa application, requests go through four layers in the stack. In order of entry, those are:
+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.
-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.
+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.
-These layers of stack can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md).
+This separation ensures that the server instance remains responsive to incoming requests, while the worker instance processes tasks in the background.
-
+
***
-## Database Layer
+## How to Set Worker Mode
-The Medusa application injects into each module, including your [custom modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), a connection to the configured PostgreSQL database. Modules use that connection to read and write data to the database.
+You 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:
-Modules can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md).
+- `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
+```
+
+
+# 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).
***
-## Third-Party Integrations Layer
+## Plugin vs Module
-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).
+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.
-Modules can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md).
+A plugin, on the other hand, can contain multiple Medusa customizations, including modules. Your plugin can define a module, then build flows around it.
-### Commerce Modules
+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.
-[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 want to reuse your Medusa customizations across multiple projects.
+- You want to share your Medusa customizations with the community.
-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:
-
-- [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.
-
-
+- 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.
***
-## Full Diagram of Medusa's Architecture
+## How to Create a Plugin?
-The following diagram illustrates Medusa's architecture including all its layers.
-
-
+The next chapter explains how you can create and publish a plugin.
# Workflows
@@ -4557,98 +4649,6 @@ You can now execute this workflow in a custom API route, scheduled job, or subsc
Find a full list of the registered resources in the Medusa container and their registration key in [this reference](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md). You can use these resources in your custom workflows.
-# Worker Mode of Medusa Instance
-
-In this chapter, you'll learn about the different modes of running a Medusa instance and how to configure the mode.
-
-## What is Worker Mode?
-
-By default, the Medusa application runs both the server, which handles all incoming requests, and the worker, which processes background tasks, in a single process. While this setup is suitable for development, it is not optimal for production environments where background tasks can be long-running or resource-intensive.
-
-In a production environment, you should deploy two separate instances of your Medusa application:
-
-1. A server instance that handles incoming requests to the application's API routes.
-2. A worker instance that processes background tasks. This includes scheduled jobs and subscribers.
-
-You don't need to set up different projects for each instance. Instead, you can configure the Medusa application to run in different modes based on environment variables, as you'll see later in this chapter.
-
-This separation ensures that the server instance remains responsive to incoming requests, while the worker instance processes tasks in the background.
-
-
-
-***
-
-## 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.
@@ -4739,6 +4739,125 @@ MEDUSA_FF_ANALYTICS=false
```
+# Write Tests for Modules
+
+In this chapter, you'll learn about `moduleIntegrationTestRunner` from Medusa's Testing Framework and how to use it to write integration tests for a module's main service.
+
+### Prerequisites
+
+- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md)
+
+## moduleIntegrationTestRunner Utility
+
+`moduleIntegrationTestRunner` creates integration tests for a module. The integration tests run on a test Medusa application with only the specified module enabled.
+
+For example, assuming you have a `blog` module, create a test file at `src/modules/blog/__tests__/service.spec.ts`:
+
+```ts title="src/modules/blog/__tests__/service.spec.ts"
+import { moduleIntegrationTestRunner } from "@medusajs/test-utils"
+import { BLOG_MODULE } from ".."
+import BlogModuleService from "../service"
+import Post from "../models/post"
+
+moduleIntegrationTestRunner({
+ moduleName: BLOG_MODULE,
+ moduleModels: [Post],
+ resolve: "./src/modules/blog",
+ testSuite: ({ service }) => {
+ // TODO write tests
+ },
+})
+
+jest.setTimeout(60 * 1000)
+```
+
+The `moduleIntegrationTestRunner` function accepts as a parameter an object with the following properties:
+
+- `moduleName`: The name of the module.
+- `moduleModels`: An array of models in the module. Refer to [this section](#write-tests-for-modules-without-data-models) if your module doesn't have data models.
+- `resolve`: The path to the module's directory.
+- `testSuite`: A function that defines the tests to run.
+
+The `testSuite` function accepts as a parameter an object having the `service` property, which is an instance of the module's main service.
+
+The type argument provided to the `moduleIntegrationTestRunner` function is used as the type of the `service` property.
+
+The tests in the `testSuite` function are written using [Jest](https://jestjs.io/).
+
+***
+
+## Run Tests
+
+Run the following command to run your module integration tests:
+
+```bash npm2yarn
+npm run test:integration:modules
+```
+
+If you don't have a `test:integration:modules` script in `package.json`, refer to the [Medusa Testing Tools chapter](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools#add-test-commands/index.html.md).
+
+This runs your Medusa application and runs the tests available in any `__tests__` directory under the `src/modules` directory.
+
+***
+
+## Pass Module Options
+
+If your module accepts options, you can set them using the `moduleOptions` property of the `moduleIntegrationTestRunner`'s parameter.
+
+For example:
+
+```ts
+import { moduleIntegrationTestRunner } from "@medusajs/test-utils"
+import BlogModuleService from "../service"
+
+moduleIntegrationTestRunner({
+ moduleOptions: {
+ apiKey: "123",
+ },
+ // ...
+})
+```
+
+***
+
+## Write Tests for Modules without Data Models
+
+If your module doesn't have a data model, pass a dummy model in the `moduleModels` property.
+
+For example:
+
+```ts
+import { moduleIntegrationTestRunner } from "@medusajs/test-utils"
+import BlogModuleService from "../service"
+import { model } from "@medusajs/framework/utils"
+
+const DummyModel = model.define("dummy_model", {
+ id: model.id().primaryKey(),
+})
+
+moduleIntegrationTestRunner({
+ moduleModels: [DummyModel],
+ // ...
+})
+
+jest.setTimeout(60 * 1000)
+```
+
+***
+
+### Other Options and Inputs
+
+Refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/moduleIntegrationTestRunner/index.html.md) for other available parameter options and inputs of the `testSuite` function.
+
+***
+
+## Database Used in Tests
+
+The `moduleIntegrationTestRunner` function creates a database with a random name before running the tests. Then, it drops that database after all the tests end.
+
+To manage that database, such as changing its name or perform operations on it in your tests, refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/moduleIntegrationTestRunner/index.html.md).
+
+
# Guide: Create Brand API Route
In the previous two chapters, you created a [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) that added the concepts of brands to your application, then created a [workflow to create a brand](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md). In this chapter, you'll expose an API route that allows admin users to create a brand using the workflow from the previous chapter.
@@ -5105,142 +5224,86 @@ The Brand Module now creates a `brand` table in the database and provides a clas
In the next chapter, you'll implement the functionality to create a brand in a workflow. You'll then use that workflow in a later chapter to expose an endpoint that allows admin users to create a brand.
-# Guide: Create Brand Workflow
+# Write Integration Tests
-This chapter builds on the work from the [previous chapter](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) where you created a Brand Module.
-
-After adding custom modules to your application, you build commerce features around them using workflows. A workflow is a series of queries and actions, called steps, that complete a task spanning across modules. You construct a workflow similar to a regular function, but it's a special function that allows you to define roll-back logic, retry configurations, and more advanced features.
-
-The workflow you'll create in this chapter will use the Brand Module's service to implement the feature of creating a brand. In the [next chapter](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md), you'll expose an API route that allows admin users to create a brand, and you'll use this workflow in the route's implementation.
-
-Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md).
+In this chapter, you'll learn about `medusaIntegrationTestRunner` from Medusa's Testing Framework and how to use it to write integration tests.
### Prerequisites
-- [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md)
+- [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)
+```
***
-## 1. Create createBrandStep
+### Run Tests
-A workflow consists of a series of steps, each step created in a TypeScript or JavaScript file under the `src/workflows` directory. A step is defined using `createStep` from the Workflows SDK
+Run the following command to run your tests:
-The workflow you're creating in this guide has one step to create the brand. So, create the file `src/workflows/create-brand.ts` with the following content:
-
-
-
-```ts title="src/workflows/create-brand.ts"
-import {
- createStep,
- StepResponse,
-} from "@medusajs/framework/workflows-sdk"
-import { BRAND_MODULE } from "../modules/brand"
-import BrandModuleService from "../modules/brand/service"
-
-export type CreateBrandStepInput = {
- name: string
-}
-
-export const createBrandStep = createStep(
- "create-brand-step",
- async (input: CreateBrandStepInput, { container }) => {
- const brandModuleService: BrandModuleService = container.resolve(
- BRAND_MODULE
- )
-
- const brand = await brandModuleService.createBrands(input)
-
- return new StepResponse(brand, brand.id)
- }
-)
+```bash npm2yarn
+npm run test:integration
```
-You create a `createBrandStep` using the `createStep` function. It accepts the step's unique name as a first parameter, and the step's function as a second parameter.
+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).
-The step function receives two parameters: input passed to the step when it's invoked, and an object of general context and configurations. This object has a `container` property, which is the Medusa container.
-
-The [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) is a registry of Framework and commerce tools accessible in your customizations, such as a workflow's step. The Medusa application registers the services of core and custom modules in the container, allowing you to resolve and use them.
-
-So, In the step function, you use the Medusa container to resolve the Brand Module's service and use its generated `createBrands` method, which accepts an object of brands to create.
-
-Learn more about the generated `create` method's usage in [this reference](https://docs.medusajs.com/resources/service-factory-reference/methods/create/index.html.md).
-
-A step must return an instance of `StepResponse`. Its first parameter is the data returned by the step, and the second is the data passed to the compensation function, which you'll learn about next.
-
-### Add Compensation Function to Step
-
-You define for each step a compensation function that's executed when an error occurs in the workflow. The compensation function defines the logic to roll-back the changes made by the step. This ensures your data remains consistent if an error occurs, which is especially useful when you integrate third-party services.
-
-Learn more about the compensation function in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md).
-
-To add a compensation function to the `createBrandStep`, pass it as a third parameter to `createStep`:
-
-```ts title="src/workflows/create-brand.ts"
-export const createBrandStep = createStep(
- // ...
- async (id: string, { container }) => {
- const brandModuleService: BrandModuleService = container.resolve(
- BRAND_MODULE
- )
-
- await brandModuleService.deleteBrands(id)
- }
-)
-```
-
-The compensation function's first parameter is the brand's ID which you passed as a second parameter to the step function's returned `StepResponse`. It also accepts a context object with a `container` property as a second parameter, similar to the step function.
-
-In the compensation function, you resolve the Brand Module's service from the Medusa container, then use its generated `deleteBrands` method to delete the brand created by the step. This method accepts the ID of the brand to delete.
-
-Learn more about the generated `delete` method's usage in [this reference](https://docs.medusajs.com/resources/service-factory-reference/methods/delete/index.html.md).
-
-So, if an error occurs during the workflow's execution, the brand that was created by the step is deleted to maintain data consistency.
+This runs your Medusa application and runs the tests available under the `src/integrations/http` directory.
***
-## 2. Create createBrandWorkflow
+## Other Options and Inputs
-You can now create the workflow that runs the `createBrandStep`. A workflow is created in a TypeScript or JavaScript file under the `src/workflows` directory. In the file, you use `createWorkflow` from the Workflows SDK to create the workflow.
-
-Add the following content in the same `src/workflows/create-brand.ts` file:
-
-```ts title="src/workflows/create-brand.ts"
-// other imports...
-import {
- // ...
- createWorkflow,
- WorkflowResponse,
-} from "@medusajs/framework/workflows-sdk"
-
-// ...
-
-type CreateBrandWorkflowInput = {
- name: string
-}
-
-export const createBrandWorkflow = createWorkflow(
- "create-brand",
- (input: CreateBrandWorkflowInput) => {
- const brand = createBrandStep(input)
-
- return new WorkflowResponse(brand)
- }
-)
-```
-
-You create the `createBrandWorkflow` using the `createWorkflow` function. This function accepts two parameters: the workflow's unique name, and the workflow's constructor function holding the workflow's implementation.
-
-The constructor function accepts the workflow's input as a parameter. In the function, you invoke the `createBrandStep` you created in the previous step to create a brand.
-
-A workflow must return an instance of `WorkflowResponse`. It accepts as a parameter the data to return to the workflow's executor.
+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.
***
-## Next Steps: Expose Create Brand API Route
+## Database Used in Tests
-You now have a `createBrandWorkflow` that you can execute to create a brand.
+The `medusaIntegrationTestRunner` function creates a database with a random name before running the tests. Then, it drops that database after all the tests end.
-In the next chapter, you'll add an API route that allows admin users to create a brand. You'll learn how to create the API route, and execute in it the workflow you implemented in this chapter.
+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.
# Create Brands UI Route in Admin
@@ -5615,6 +5678,144 @@ Your customizations often span across systems, where you need to retrieve data o
In the next chapters, you'll learn about the concepts that facilitate integrating third-party systems in your application. You'll integrate a dummy third-party system and sync the brands between it and the Medusa application.
+# Guide: Create Brand Workflow
+
+This chapter builds on the work from the [previous chapter](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) where you created a Brand Module.
+
+After adding custom modules to your application, you build commerce features around them using workflows. A workflow is a series of queries and actions, called steps, that complete a task spanning across modules. You construct a workflow similar to a regular function, but it's a special function that allows you to define roll-back logic, retry configurations, and more advanced features.
+
+The workflow you'll create in this chapter will use the Brand Module's service to implement the feature of creating a brand. In the [next chapter](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md), you'll expose an API route that allows admin users to create a brand, and you'll use this workflow in the route's implementation.
+
+Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md).
+
+### Prerequisites
+
+- [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md)
+
+***
+
+## 1. Create createBrandStep
+
+A workflow consists of a series of steps, each step created in a TypeScript or JavaScript file under the `src/workflows` directory. A step is defined using `createStep` from the Workflows SDK
+
+The workflow you're creating in this guide has one step to create the brand. So, create the file `src/workflows/create-brand.ts` with the following content:
+
+
+
+```ts title="src/workflows/create-brand.ts"
+import {
+ createStep,
+ StepResponse,
+} from "@medusajs/framework/workflows-sdk"
+import { BRAND_MODULE } from "../modules/brand"
+import BrandModuleService from "../modules/brand/service"
+
+export type CreateBrandStepInput = {
+ name: string
+}
+
+export const createBrandStep = createStep(
+ "create-brand-step",
+ async (input: CreateBrandStepInput, { container }) => {
+ const brandModuleService: BrandModuleService = container.resolve(
+ BRAND_MODULE
+ )
+
+ const brand = await brandModuleService.createBrands(input)
+
+ return new StepResponse(brand, brand.id)
+ }
+)
+```
+
+You create a `createBrandStep` using the `createStep` function. It accepts the step's unique name as a first parameter, and the step's function as a second parameter.
+
+The step function receives two parameters: input passed to the step when it's invoked, and an object of general context and configurations. This object has a `container` property, which is the Medusa container.
+
+The [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) is a registry of Framework and commerce tools accessible in your customizations, such as a workflow's step. The Medusa application registers the services of core and custom modules in the container, allowing you to resolve and use them.
+
+So, In the step function, you use the Medusa container to resolve the Brand Module's service and use its generated `createBrands` method, which accepts an object of brands to create.
+
+Learn more about the generated `create` method's usage in [this reference](https://docs.medusajs.com/resources/service-factory-reference/methods/create/index.html.md).
+
+A step must return an instance of `StepResponse`. Its first parameter is the data returned by the step, and the second is the data passed to the compensation function, which you'll learn about next.
+
+### Add Compensation Function to Step
+
+You define for each step a compensation function that's executed when an error occurs in the workflow. The compensation function defines the logic to roll-back the changes made by the step. This ensures your data remains consistent if an error occurs, which is especially useful when you integrate third-party services.
+
+Learn more about the compensation function in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md).
+
+To add a compensation function to the `createBrandStep`, pass it as a third parameter to `createStep`:
+
+```ts title="src/workflows/create-brand.ts"
+export const createBrandStep = createStep(
+ // ...
+ async (id: string, { container }) => {
+ const brandModuleService: BrandModuleService = container.resolve(
+ BRAND_MODULE
+ )
+
+ await brandModuleService.deleteBrands(id)
+ }
+)
+```
+
+The compensation function's first parameter is the brand's ID which you passed as a second parameter to the step function's returned `StepResponse`. It also accepts a context object with a `container` property as a second parameter, similar to the step function.
+
+In the compensation function, you resolve the Brand Module's service from the Medusa container, then use its generated `deleteBrands` method to delete the brand created by the step. This method accepts the ID of the brand to delete.
+
+Learn more about the generated `delete` method's usage in [this reference](https://docs.medusajs.com/resources/service-factory-reference/methods/delete/index.html.md).
+
+So, if an error occurs during the workflow's execution, the brand that was created by the step is deleted to maintain data consistency.
+
+***
+
+## 2. Create createBrandWorkflow
+
+You can now create the workflow that runs the `createBrandStep`. A workflow is created in a TypeScript or JavaScript file under the `src/workflows` directory. In the file, you use `createWorkflow` from the Workflows SDK to create the workflow.
+
+Add the following content in the same `src/workflows/create-brand.ts` file:
+
+```ts title="src/workflows/create-brand.ts"
+// other imports...
+import {
+ // ...
+ createWorkflow,
+ WorkflowResponse,
+} from "@medusajs/framework/workflows-sdk"
+
+// ...
+
+type CreateBrandWorkflowInput = {
+ name: string
+}
+
+export const createBrandWorkflow = createWorkflow(
+ "create-brand",
+ (input: CreateBrandWorkflowInput) => {
+ const brand = createBrandStep(input)
+
+ return new WorkflowResponse(brand)
+ }
+)
+```
+
+You create the `createBrandWorkflow` using the `createWorkflow` function. This function accepts two parameters: the workflow's unique name, and the workflow's constructor function holding the workflow's implementation.
+
+The constructor function accepts the workflow's input as a parameter. In the function, you invoke the `createBrandStep` you created in the previous step to create a brand.
+
+A workflow must return an instance of `WorkflowResponse`. It accepts as a parameter the data to return to the workflow's executor.
+
+***
+
+## Next Steps: Expose Create Brand API Route
+
+You now have a `createBrandWorkflow` that you can execute to create a brand.
+
+In the next chapter, you'll add an API route that allows admin users to create a brand. You'll learn how to create the API route, and execute in it the workflow you implemented in this chapter.
+
+
# Guide: Add Product's Brand Widget in Admin
In this chapter, you'll customize the product details page of the Medusa Admin dashboard to show the product's [brand](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md). You'll create a widget that is injected into a pre-defined zone in the page, and in the widget you'll retrieve the product's brand from the server and display it.
@@ -5839,6 +6040,426 @@ You can also run the `npx medusa db:sync-links` to just sync module links withou
In the next chapter, you'll extend Medusa's workflow and API route that create a product to allow associating a brand with a product. You'll also learn how to link brand and product records.
+# Guide: Query Product's Brands
+
+In the previous chapters, you [defined a link](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md) between the [custom Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) and Medusa's [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md), then [extended the create-product flow](https://docs.medusajs.com/learn/customization/extend-features/extend-create-product/index.html.md) to link a product to a brand.
+
+In this chapter, you'll learn how to retrieve a product's brand (and vice-versa) in two ways: Using Medusa's existing API route, or in customizations, such as a custom API route.
+
+### Prerequisites
+
+- [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md)
+- [Defined link between the Brand and Product data models.](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md)
+
+***
+
+## Approach 1: Retrieve Brands in Existing API Routes
+
+Medusa's existing API routes accept a `fields` query parameter that allows you to specify the fields and relations of a model to retrieve. So, when you send a request to the [List Products](https://docs.medusajs.com/api/admin#products_getproducts), [Get Product](https://docs.medusajs.com/api/admin#products_getproductsid), or any product-related store or admin routes that accept a `fields` query parameter, you can specify in this parameter to return the product's brands.
+
+Learn more about using the `fields` query parameter to retrieve custom linked data models in the [Retrieve Custom Linked Data Models from Medusa's API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links/index.html.md) chapter.
+
+For example, send the following request to retrieve the list of products with their brands:
+
+```bash
+curl 'http://localhost:9000/admin/products?fields=+brand.*' \
+--header 'Authorization: Bearer {token}'
+```
+
+Make sure to replace `{token}` with your admin user's authentication token. Learn how to retrieve it in the [API reference](https://docs.medusajs.com/api/store#authentication).
+
+Any product that is linked to a brand will have a `brand` property in its object:
+
+```json title="Example Product Object"
+{
+ "id": "prod_123",
+ // ...
+ "brand": {
+ "id": "01JEB44M61BRM3ARM2RRMK7GJF",
+ "name": "Acme",
+ "created_at": "2024-12-05T09:59:08.737Z",
+ "updated_at": "2024-12-05T09:59:08.737Z",
+ "deleted_at": null
+ }
+}
+```
+
+By using the `fields` query parameter, you don't have to re-create existing API routes to get custom data models that you linked to core data models.
+
+### Limitations: Filtering by Brands in Existing API Routes
+
+While you can retrieve linked records using the `fields` query parameter of an existing API route, you can't filter by linked records.
+
+Instead, you'll have to create a custom API route that uses Query to retrieve linked records with filters, as explained in the [Query documentation](https://docs.medusajs.com/learn/fundamentals/module-links/query#apply-filters-and-pagination-on-linked-records/index.html.md).
+
+***
+
+## Approach 2: Use Query to Retrieve Linked Records
+
+You can also retrieve linked records using Query. Query allows you to retrieve data across modules with filters, pagination, and more. You can resolve Query from the Medusa container and use it in your API route or workflow.
+
+Learn more about Query in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md).
+
+For example, you can create an API route that retrieves brands and their products. If you followed the [Create Brands API route chapter](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md), you'll have the file `src/api/admin/brands/route.ts` with a `POST` API route. Add a new `GET` function to the same file:
+
+```ts title="src/api/admin/brands/route.ts" highlights={highlights}
+// other imports...
+import {
+ MedusaRequest,
+ MedusaResponse,
+} from "@medusajs/framework/http"
+
+export const GET = async (
+ req: MedusaRequest,
+ res: MedusaResponse
+) => {
+ const query = req.scope.resolve("query")
+
+ const { data: brands } = await query.graph({
+ entity: "brand",
+ fields: ["*", "products.*"],
+ })
+
+ res.json({ brands })
+}
+```
+
+This adds a `GET` API route at `/admin/brands`. In the API route, you resolve Query from the Medusa container. Query has a `graph` method that runs a query to retrieve data. It accepts an object having the following properties:
+
+- `entity`: The data model's name as specified in the first parameter of `model.define`.
+- `fields`: An array of properties and relations to retrieve. You can pass:
+ - A property's name, such as `id`, or `*` for all properties.
+ - A relation or linked model's name, such as `products` (use the plural name since brands are linked to list of products). You suffix the name with `.*` to retrieve all its properties.
+
+`graph` returns an object having a `data` property, which is the retrieved brands. You return the brands in the response.
+
+### Test it Out
+
+To test the API route out, send a `GET` request to `/admin/brands`:
+
+```bash
+curl 'http://localhost:9000/admin/brands' \
+-H 'Authorization: Bearer {token}'
+```
+
+Make sure to replace `{token}` with your admin user's authentication token. Learn how to retrieve it in the [API reference](https://docs.medusajs.com/api/store#authentication).
+
+This returns the brands in your store with their linked products. For example:
+
+```json title="Example Response"
+{
+ "brands": [
+ {
+ "id": "123",
+ // ...
+ "products": [
+ {
+ "id": "prod_123",
+ // ...
+ }
+ ]
+ }
+ ]
+}
+```
+
+### Limitations: Filtering by Brand in Query
+
+While you can use Query to retrieve linked records, you can't filter by linked records.
+
+For an alternative approach, refer to the [Query documentation](https://docs.medusajs.com/learn/fundamentals/module-links/query#apply-filters-and-pagination-on-linked-records/index.html.md).
+
+***
+
+## Summary
+
+By following the examples of the previous chapters, you:
+
+- Defined a link between the Brand and Product modules's data models, allowing you to associate a product with a brand.
+- Extended the create-product workflow and route to allow setting the product's brand while creating the product.
+- Queried a product's brand, and vice versa.
+
+***
+
+## Next Steps: Customize Medusa Admin
+
+Clients, such as the Medusa Admin dashboard, can now use brand-related features, such as creating a brand or setting the brand of a product.
+
+In the next chapters, you'll learn how to customize the Medusa Admin to show a product's brand on its details page, and to show a new page with the list of brands in your store.
+
+
+# Guide: Sync Brands from Medusa to Third-Party
+
+In the [previous chapter](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md), you created a CMS Module that integrates a dummy third-party system. You can now perform actions using that module within your custom flows.
+
+In another previous chapter, you [added a workflow](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md) that creates a brand. After integrating the CMS, you want to sync that brand to the third-party system as well.
+
+Medusa has an event system that emits events when an operation is performed. It allows you to listen to those events and perform an asynchronous action in a function called a [subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). This is useful to perform actions that aren't integral to the original flow, such as syncing data to a third-party system.
+
+Learn more about Medusa's event system and subscribers in [this chapter](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md).
+
+In this chapter, you'll modify the `createBrandWorkflow` you created before to emit a custom event that indicates a brand was created. Then, you'll listen to that event in a subscriber to sync the brand to the third-party CMS. You'll implement the sync logic within a workflow that you execute in the subscriber.
+
+### Prerequisites
+
+- [createBrandWorkflow](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md)
+- [CMS Module](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md)
+
+## 1. Emit Event in createBrandWorkflow
+
+Since syncing the brand to the third-party system isn't integral to creating a brand, you'll emit a custom event indicating that a brand was created.
+
+Medusa provides an `emitEventStep` that allows you to emit an event in your workflows. So, in the `createBrandWorkflow` defined in `src/workflows/create-brand.ts`, use the `emitEventStep` helper step after the `createBrandStep`:
+
+```ts title="src/workflows/create-brand.ts" highlights={eventHighlights}
+// other imports...
+import {
+ emitEventStep,
+} from "@medusajs/medusa/core-flows"
+
+// ...
+
+export const createBrandWorkflow = createWorkflow(
+ "create-brand",
+ (input: CreateBrandInput) => {
+ // ...
+
+ emitEventStep({
+ eventName: "brand.created",
+ data: {
+ id: brand.id,
+ },
+ })
+
+ return new WorkflowResponse(brand)
+ }
+)
+```
+
+The `emitEventStep` accepts an object parameter having two properties:
+
+- `eventName`: The name of the event to emit. You'll use this name later to listen to the event in a subscriber.
+- `data`: The data payload to emit with the event. This data is passed to subscribers that listen to the event. You add the brand's ID to the data payload, informing the subscribers which brand was created.
+
+You'll learn how to handle this event in a later step.
+
+***
+
+## 2. Create Sync to Third-Party System Workflow
+
+The subscriber that will listen to the `brand.created` event will sync the created brand to the third-party CMS. So, you'll implement the syncing logic in a workflow, then execute the workflow in the subscriber.
+
+Workflows have a built-in durable execution engine that helps you complete tasks spanning multiple systems. Also, their rollback mechanism ensures that data is consistent across systems even when errors occur during execution.
+
+Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md).
+
+You'll create a `syncBrandToSystemWorkflow` that has two steps:
+
+- `useQueryGraphStep`: a step that Medusa provides to retrieve data using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). You'll use this to retrieve the brand's details using its ID.
+- `syncBrandToCmsStep`: a step that you'll create to sync the brand to the CMS.
+
+### syncBrandToCmsStep
+
+To implement the step that syncs the brand to the CMS, create the file `src/workflows/sync-brands-to-cms.ts` with the following content:
+
+
+
+```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.
+
+
# 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.
@@ -6051,154 +6672,6 @@ In the Medusa application's logs, you'll find the message `Linked brand to produ
Now that you've extending the create-product flow to link a brand to it, you want to retrieve the brand details of a product. You'll learn how to do so in the next chapter.
-# Guide: Query Product's Brands
-
-In the previous chapters, you [defined a link](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md) between the [custom Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) and Medusa's [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md), then [extended the create-product flow](https://docs.medusajs.com/learn/customization/extend-features/extend-create-product/index.html.md) to link a product to a brand.
-
-In this chapter, you'll learn how to retrieve a product's brand (and vice-versa) in two ways: Using Medusa's existing API route, or in customizations, such as a custom API route.
-
-### Prerequisites
-
-- [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md)
-- [Defined link between the Brand and Product data models.](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md)
-
-***
-
-## Approach 1: Retrieve Brands in Existing API Routes
-
-Medusa's existing API routes accept a `fields` query parameter that allows you to specify the fields and relations of a model to retrieve. So, when you send a request to the [List Products](https://docs.medusajs.com/api/admin#products_getproducts), [Get Product](https://docs.medusajs.com/api/admin#products_getproductsid), or any product-related store or admin routes that accept a `fields` query parameter, you can specify in this parameter to return the product's brands.
-
-Learn more about using the `fields` query parameter to retrieve custom linked data models in the [Retrieve Custom Linked Data Models from Medusa's API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links/index.html.md) chapter.
-
-For example, send the following request to retrieve the list of products with their brands:
-
-```bash
-curl 'http://localhost:9000/admin/products?fields=+brand.*' \
---header 'Authorization: Bearer {token}'
-```
-
-Make sure to replace `{token}` with your admin user's authentication token. Learn how to retrieve it in the [API reference](https://docs.medusajs.com/api/store#authentication).
-
-Any product that is linked to a brand will have a `brand` property in its object:
-
-```json title="Example Product Object"
-{
- "id": "prod_123",
- // ...
- "brand": {
- "id": "01JEB44M61BRM3ARM2RRMK7GJF",
- "name": "Acme",
- "created_at": "2024-12-05T09:59:08.737Z",
- "updated_at": "2024-12-05T09:59:08.737Z",
- "deleted_at": null
- }
-}
-```
-
-By using the `fields` query parameter, you don't have to re-create existing API routes to get custom data models that you linked to core data models.
-
-### Limitations: Filtering by Brands in Existing API Routes
-
-While you can retrieve linked records using the `fields` query parameter of an existing API route, you can't filter by linked records.
-
-Instead, you'll have to create a custom API route that uses Query to retrieve linked records with filters, as explained in the [Query documentation](https://docs.medusajs.com/learn/fundamentals/module-links/query#apply-filters-and-pagination-on-linked-records/index.html.md).
-
-***
-
-## Approach 2: Use Query to Retrieve Linked Records
-
-You can also retrieve linked records using Query. Query allows you to retrieve data across modules with filters, pagination, and more. You can resolve Query from the Medusa container and use it in your API route or workflow.
-
-Learn more about Query in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md).
-
-For example, you can create an API route that retrieves brands and their products. If you followed the [Create Brands API route chapter](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md), you'll have the file `src/api/admin/brands/route.ts` with a `POST` API route. Add a new `GET` function to the same file:
-
-```ts title="src/api/admin/brands/route.ts" highlights={highlights}
-// other imports...
-import {
- MedusaRequest,
- MedusaResponse,
-} from "@medusajs/framework/http"
-
-export const GET = async (
- req: MedusaRequest,
- res: MedusaResponse
-) => {
- const query = req.scope.resolve("query")
-
- const { data: brands } = await query.graph({
- entity: "brand",
- fields: ["*", "products.*"],
- })
-
- res.json({ brands })
-}
-```
-
-This adds a `GET` API route at `/admin/brands`. In the API route, you resolve Query from the Medusa container. Query has a `graph` method that runs a query to retrieve data. It accepts an object having the following properties:
-
-- `entity`: The data model's name as specified in the first parameter of `model.define`.
-- `fields`: An array of properties and relations to retrieve. You can pass:
- - A property's name, such as `id`, or `*` for all properties.
- - A relation or linked model's name, such as `products` (use the plural name since brands are linked to list of products). You suffix the name with `.*` to retrieve all its properties.
-
-`graph` returns an object having a `data` property, which is the retrieved brands. You return the brands in the response.
-
-### Test it Out
-
-To test the API route out, send a `GET` request to `/admin/brands`:
-
-```bash
-curl 'http://localhost:9000/admin/brands' \
--H 'Authorization: Bearer {token}'
-```
-
-Make sure to replace `{token}` with your admin user's authentication token. Learn how to retrieve it in the [API reference](https://docs.medusajs.com/api/store#authentication).
-
-This returns the brands in your store with their linked products. For example:
-
-```json title="Example Response"
-{
- "brands": [
- {
- "id": "123",
- // ...
- "products": [
- {
- "id": "prod_123",
- // ...
- }
- ]
- }
- ]
-}
-```
-
-### Limitations: Filtering by Brand in Query
-
-While you can use Query to retrieve linked records, you can't filter by linked records.
-
-For an alternative approach, refer to the [Query documentation](https://docs.medusajs.com/learn/fundamentals/module-links/query#apply-filters-and-pagination-on-linked-records/index.html.md).
-
-***
-
-## Summary
-
-By following the examples of the previous chapters, you:
-
-- Defined a link between the Brand and Product modules's data models, allowing you to associate a product with a brand.
-- Extended the create-product workflow and route to allow setting the product's brand while creating the product.
-- Queried a product's brand, and vice versa.
-
-***
-
-## Next Steps: Customize Medusa Admin
-
-Clients, such as the Medusa Admin dashboard, can now use brand-related features, such as creating a brand or setting the brand of a product.
-
-In the next chapters, you'll learn how to customize the Medusa Admin to show a product's brand on its details page, and to show a new page with the list of brands in your store.
-
-
# Guide: Schedule Syncing Brands from Third-Party
In the previous chapters, you've [integrated a third-party CMS](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md) and implemented the logic to [sync created brands](https://docs.medusajs.com/learn/customization/integrate-systems/handle-event/index.html.md) from Medusa to the CMS.
@@ -6508,276 +6981,126 @@ 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
+# Admin Development Constraints
-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.
+This chapter lists some constraints of admin widgets and UI routes.
-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.
+## Arrow Functions
-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.
+Widget and UI route components must be created as arrow functions.
-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 {
+```ts highlights={arrowHighlights}
+// Don't
+function ProductWidget() {
// ...
- 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({})
- }
-)
+// Do
+const ProductWidget = () => {
+ // ...
+}
```
-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
+## Widget Zone
-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.
+A widget zone's value must be wrapped in double or single quotes. It can't be a template literal or a variable.
-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 highlights={zoneHighlights}
+// Don't
+export const config = defineWidgetConfig({
+ zone: `product.details.before`,
+})
-
+// Don't
+const ZONE = "product.details.after"
+export const config = defineWidgetConfig({
+ zone: ZONE,
+})
-```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",
-}
+// Do
+export const config = defineWidgetConfig({
+ zone: "product.details.before",
+})
```
-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.
+# Environment Variables in Admin Customizations
-The subscriber function accepts an object parameter that has two properties:
+In this chapter, you'll learn how to use environment variables in your admin customizations.
-- `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.
+To learn how environment variables are generally loaded in Medusa based on your application's environment, check out [this chapter](https://docs.medusajs.com/learn/fundamentals/environment-variables/index.html.md).
-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.
+## How to Set Environment Variables
-Learn more about subscribers in [this chapter](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md).
+The Medusa Admin is built on top of [Vite](https://vite.dev/). To set an environment variable that you want to use in a widget or UI route, prefix the environment variable with `VITE_`.
-***
-
-## 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:
+For example:
```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"
+VITE_MY_API_KEY=sk_123
```
***
-## Next Chapter: Sync Brand from Third-Party CMS to Medusa
+## How to Use Environment Variables
-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.
+To access or use an environment variable starting with `VITE_`, use the `import.meta.env` object.
+
+For example:
+
+```tsx highlights={[["8"]]}
+import { defineWidgetConfig } from "@medusajs/admin-sdk"
+import { Container, Heading } from "@medusajs/ui"
+
+const ProductWidget = () => {
+ return (
+
+
+ API Key: {import.meta.env.VITE_MY_API_KEY}
+
+
+ )
+}
+
+export const config = defineWidgetConfig({
+ zone: "product.details.before",
+})
+
+export default ProductWidget
+```
+
+In this example, you display the API key in a widget using `import.meta.env.VITE_MY_API_KEY`.
+
+### Type Error on import.meta.env
+
+If you receive a type error on `import.meta.env`, create the file `src/admin/vite-env.d.ts` with the following content:
+
+```ts title="src/admin/vite-env.d.ts"
+///
+```
+
+This file tells TypeScript to recognize the `import.meta.env` object and enhances the types of your custom environment variables.
+
+***
+
+## Check Node Environment in Admin Customizations
+
+To check the current environment, Vite exposes two variables:
+
+- `import.meta.env.DEV`: Returns `true` if the current environment is development.
+- `import.meta.env.PROD`: Returns `true` if the current environment is production.
+
+Learn more about other Vite environment variables in the [Vite documentation](https://vite.dev/guide/env-and-mode).
+
+***
+
+## Environment Variables in Production
+
+When you build the Medusa application, including the Medusa Admin, with the `build` command, the environment variables are inlined into the build. This means that you can't change the environment variables without rebuilding the application.
+
+For example, the `VITE_MY_API_KEY` environment variable in the example above will be replaced with the actual value during the build process.
# Guide: Integrate Third-Party Brand System
@@ -6939,329 +7262,6 @@ You can now use the CMS Module's service to perform actions on the third-party C
In the next chapter, you'll learn how to emit an event when a brand is created, then handle that event to sync the brand from Medusa to the third-party service.
-# Write Integration Tests
-
-In this chapter, you'll learn about `medusaIntegrationTestRunner` from Medusa's Testing Framework and how to use it to write integration tests.
-
-### Prerequisites
-
-- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md)
-
-## medusaIntegrationTestRunner Utility
-
-The `medusaIntegrationTestRunner` is from Medusa's Testing Framework and it's used to create integration tests in your Medusa project. It runs a full Medusa application, allowing you test API routes, workflows, or other customizations.
-
-For example:
-
-```ts title="integration-tests/http/test.spec.ts" highlights={highlights}
-import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
-
-medusaIntegrationTestRunner({
- testSuite: ({ api, getContainer }) => {
- // TODO write tests...
- },
-})
-
-jest.setTimeout(60 * 1000)
-```
-
-The `medusaIntegrationTestRunner` function accepts an object as a parameter. The object has a required property `testSuite`.
-
-`testSuite`'s value is a function that defines the tests to run. The function accepts as a parameter an object that has the following properties:
-
-- `api`: a set of utility methods used to send requests to the Medusa application. It has the following methods:
- - `get`: Send a `GET` request to an API route.
- - `post`: Send a `POST` request to an API route.
- - `delete`: Send a `DELETE` request to an API route.
-- `getContainer`: a function that retrieves the Medusa Container. Use the `getContainer().resolve` method to resolve resources from the Medusa Container.
-
-The tests in the `testSuite` function are written using [Jest](https://jestjs.io/).
-
-### Jest Timeout
-
-Since your tests connect to the database and perform actions that require more time than the typical tests, make sure to increase the timeout in your test:
-
-```ts title="integration-tests/http/test.spec.ts"
-// in your test's file
-jest.setTimeout(60 * 1000)
-```
-
-***
-
-### Run Tests
-
-Run the following command to run your tests:
-
-```bash npm2yarn
-npm run test:integration
-```
-
-If you don't have a `test:integration` script in `package.json`, refer to the [Medusa Testing Tools chapter](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools#add-test-commands/index.html.md).
-
-This runs your Medusa application and runs the tests available under the `src/integrations/http` directory.
-
-***
-
-## Other Options and Inputs
-
-Refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/medusaIntegrationTestRunner/index.html.md) for other available parameter options and inputs of the `testSuite` function.
-
-***
-
-## Database Used in Tests
-
-The `medusaIntegrationTestRunner` function creates a database with a random name before running the tests. Then, it drops that database after all the tests end.
-
-To manage that database, such as changing its name or perform operations on it in your tests, refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/medusaIntegrationTestRunner/index.html.md).
-
-***
-
-## Example Integration Tests
-
-The next chapters provide examples of writing integration tests for API routes and workflows.
-
-
-# Write Tests for Modules
-
-In this chapter, you'll learn about `moduleIntegrationTestRunner` from Medusa's Testing Framework and how to use it to write integration tests for a module's main service.
-
-### Prerequisites
-
-- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md)
-
-## moduleIntegrationTestRunner Utility
-
-`moduleIntegrationTestRunner` creates integration tests for a module. The integration tests run on a test Medusa application with only the specified module enabled.
-
-For example, assuming you have a `blog` module, create a test file at `src/modules/blog/__tests__/service.spec.ts`:
-
-```ts title="src/modules/blog/__tests__/service.spec.ts"
-import { moduleIntegrationTestRunner } from "@medusajs/test-utils"
-import { BLOG_MODULE } from ".."
-import BlogModuleService from "../service"
-import Post from "../models/post"
-
-moduleIntegrationTestRunner({
- moduleName: BLOG_MODULE,
- moduleModels: [Post],
- resolve: "./src/modules/blog",
- testSuite: ({ service }) => {
- // TODO write tests
- },
-})
-
-jest.setTimeout(60 * 1000)
-```
-
-The `moduleIntegrationTestRunner` function accepts as a parameter an object with the following properties:
-
-- `moduleName`: The name of the module.
-- `moduleModels`: An array of models in the module. Refer to [this section](#write-tests-for-modules-without-data-models) if your module doesn't have data models.
-- `resolve`: The path to the module's directory.
-- `testSuite`: A function that defines the tests to run.
-
-The `testSuite` function accepts as a parameter an object having the `service` property, which is an instance of the module's main service.
-
-The type argument provided to the `moduleIntegrationTestRunner` function is used as the type of the `service` property.
-
-The tests in the `testSuite` function are written using [Jest](https://jestjs.io/).
-
-***
-
-## Run Tests
-
-Run the following command to run your module integration tests:
-
-```bash npm2yarn
-npm run test:integration:modules
-```
-
-If you don't have a `test:integration:modules` script in `package.json`, refer to the [Medusa Testing Tools chapter](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools#add-test-commands/index.html.md).
-
-This runs your Medusa application and runs the tests available in any `__tests__` directory under the `src/modules` directory.
-
-***
-
-## Pass Module Options
-
-If your module accepts options, you can set them using the `moduleOptions` property of the `moduleIntegrationTestRunner`'s parameter.
-
-For example:
-
-```ts
-import { moduleIntegrationTestRunner } from "@medusajs/test-utils"
-import BlogModuleService from "../service"
-
-moduleIntegrationTestRunner({
- moduleOptions: {
- apiKey: "123",
- },
- // ...
-})
-```
-
-***
-
-## Write Tests for Modules without Data Models
-
-If your module doesn't have a data model, pass a dummy model in the `moduleModels` property.
-
-For example:
-
-```ts
-import { moduleIntegrationTestRunner } from "@medusajs/test-utils"
-import BlogModuleService from "../service"
-import { model } from "@medusajs/framework/utils"
-
-const DummyModel = model.define("dummy_model", {
- id: model.id().primaryKey(),
-})
-
-moduleIntegrationTestRunner({
- moduleModels: [DummyModel],
- // ...
-})
-
-jest.setTimeout(60 * 1000)
-```
-
-***
-
-### Other Options and Inputs
-
-Refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/moduleIntegrationTestRunner/index.html.md) for other available parameter options and inputs of the `testSuite` function.
-
-***
-
-## Database Used in Tests
-
-The `moduleIntegrationTestRunner` function creates a database with a random name before running the tests. Then, it drops that database after all the tests end.
-
-To manage that database, such as changing its name or perform operations on it in your tests, refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/moduleIntegrationTestRunner/index.html.md).
-
-
-# Environment Variables in Admin Customizations
-
-In this chapter, you'll learn how to use environment variables in your admin customizations.
-
-To learn how environment variables are generally loaded in Medusa based on your application's environment, check out [this chapter](https://docs.medusajs.com/learn/fundamentals/environment-variables/index.html.md).
-
-## How to Set Environment Variables
-
-The Medusa Admin is built on top of [Vite](https://vite.dev/). To set an environment variable that you want to use in a widget or UI route, prefix the environment variable with `VITE_`.
-
-For example:
-
-```plain
-VITE_MY_API_KEY=sk_123
-```
-
-***
-
-## How to Use Environment Variables
-
-To access or use an environment variable starting with `VITE_`, use the `import.meta.env` object.
-
-For example:
-
-```tsx highlights={[["8"]]}
-import { defineWidgetConfig } from "@medusajs/admin-sdk"
-import { Container, Heading } from "@medusajs/ui"
-
-const ProductWidget = () => {
- return (
-
-
- API Key: {import.meta.env.VITE_MY_API_KEY}
-
-
- )
-}
-
-export const config = defineWidgetConfig({
- zone: "product.details.before",
-})
-
-export default ProductWidget
-```
-
-In this example, you display the API key in a widget using `import.meta.env.VITE_MY_API_KEY`.
-
-### Type Error on import.meta.env
-
-If you receive a type error on `import.meta.env`, create the file `src/admin/vite-env.d.ts` with the following content:
-
-```ts title="src/admin/vite-env.d.ts"
-///
-```
-
-This file tells TypeScript to recognize the `import.meta.env` object and enhances the types of your custom environment variables.
-
-***
-
-## Check Node Environment in Admin Customizations
-
-To check the current environment, Vite exposes two variables:
-
-- `import.meta.env.DEV`: Returns `true` if the current environment is development.
-- `import.meta.env.PROD`: Returns `true` if the current environment is production.
-
-Learn more about other Vite environment variables in the [Vite documentation](https://vite.dev/guide/env-and-mode).
-
-***
-
-## Environment Variables in Production
-
-When you build the Medusa application, including the Medusa Admin, with the `build` command, the environment variables are inlined into the build. This means that you can't change the environment variables without rebuilding the application.
-
-For example, the `VITE_MY_API_KEY` environment variable in the example above will be replaced with the actual value during the build process.
-
-
-# Admin Development Constraints
-
-This chapter lists some constraints of admin widgets and UI routes.
-
-## Arrow Functions
-
-Widget and UI route components must be created as arrow functions.
-
-```ts highlights={arrowHighlights}
-// Don't
-function ProductWidget() {
- // ...
-}
-
-// Do
-const ProductWidget = () => {
- // ...
-}
-```
-
-***
-
-## Widget Zone
-
-A widget zone's value must be wrapped in double or single quotes. It can't be a template literal or a variable.
-
-```ts highlights={zoneHighlights}
-// Don't
-export const config = defineWidgetConfig({
- zone: `product.details.before`,
-})
-
-// Don't
-const ZONE = "product.details.after"
-export const config = defineWidgetConfig({
- zone: ZONE,
-})
-
-// Do
-export const config = defineWidgetConfig({
- zone: "product.details.before",
-})
-```
-
-
# Admin Routing Customizations
The Medusa Admin dashboard uses [React Router](https://reactrouter.com) under the hood to manage routing. So, you can have more flexibility in routing-related customizations using some of React Router's utilities, hooks, and components.
@@ -8288,6 +8288,113 @@ createProductsWorkflow.hooks.productsCreated(
This updates the products to their original state before adding the brand to their `metadata` property.
+# Throwing and Handling Errors
+
+In this guide, you'll learn how to throw errors in your Medusa application, how it affects an API route's response, and how to change the default error handler of your Medusa application.
+
+## Throw MedusaError
+
+When throwing an error in your API routes, middlewares, workflows, or any customization, throw a `MedusaError` from the Medusa Framework.
+
+The Medusa application's API route error handler then wraps your thrown error in a uniform object and returns it in the response.
+
+For example:
+
+```ts
+import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
+import { MedusaError } from "@medusajs/framework/utils"
+
+export const GET = async (
+ req: MedusaRequest,
+ res: MedusaResponse
+) => {
+ if (!req.query.q) {
+ throw new MedusaError(
+ MedusaError.Types.INVALID_DATA,
+ "The `q` query parameter is required."
+ )
+ }
+
+ // ...
+}
+```
+
+The `MedusaError` class accepts in its constructor two parameters:
+
+1. The first is the error's type. `MedusaError` has a static property `Types` that you can use. `Types` is an enum whose possible values are explained in the next section.
+2. The second is the message to show in the error response.
+
+### Error Object in Response
+
+The error object returned in the response has two properties:
+
+- `type`: The error's type.
+- `message`: The error message, if available.
+- `code`: A common snake-case code. Its values can be:
+ - `invalid_request_error` for the `DUPLICATE_ERROR` type.
+ - `api_error`: for the `DB_ERROR` type.
+ - `invalid_state_error` for `CONFLICT` error type.
+ - `unknown_error` for any unidentified error type.
+ - For other error types, this property won't be available unless you provide a code as a third parameter to the `MedusaError` constructor.
+
+### MedusaError Types
+
+|Type|Description|Status Code|
+|---|---|---|---|---|
+|\`DB\_ERROR\`|Indicates a database error.|\`500\`|
+|\`DUPLICATE\_ERROR\`|Indicates a duplicate of a record already exists. For example, when trying to create a customer whose email is registered by another customer.|\`422\`|
+|\`INVALID\_ARGUMENT\`|Indicates an error that occurred due to incorrect arguments or other unexpected state.|\`500\`|
+|\`INVALID\_DATA\`|Indicates a validation error.|\`400\`|
+|\`UNAUTHORIZED\`|Indicates that a user is not authorized to perform an action or access a route.|\`401\`|
+|\`NOT\_FOUND\`|Indicates that the requested resource, such as a route or a record, isn't found.|\`404\`|
+|\`NOT\_ALLOWED\`|Indicates that an operation isn't allowed.|\`400\`|
+|\`CONFLICT\`|Indicates that a request conflicts with another previous or ongoing request. The error message in this case is ignored for a default message.|\`409\`|
+|\`PAYMENT\_AUTHORIZATION\_ERROR\`|Indicates an error has occurred while authorizing a payment.|\`422\`|
+|Other error types|Any other error type results in an |\`500\`|
+
+***
+
+## Override Error Handler
+
+The `defineMiddlewares` function used to apply middlewares on routes accepts an `errorHandler` in its object parameter. Use it to override the default error handler for API routes.
+
+This error handler will also be used for errors thrown in Medusa's API routes and resources.
+
+For example, create `src/api/middlewares.ts` with the following:
+
+```ts title="src/api/middlewares.ts" collapsibleLines="1-8" expandMoreLabel="Show Imports"
+import {
+ defineMiddlewares,
+ MedusaNextFunction,
+ MedusaRequest,
+ MedusaResponse,
+} from "@medusajs/framework/http"
+import { MedusaError } from "@medusajs/framework/utils"
+
+export default defineMiddlewares({
+ errorHandler: (
+ error: MedusaError | any,
+ req: MedusaRequest,
+ res: MedusaResponse,
+ next: MedusaNextFunction
+ ) => {
+ res.status(400).json({
+ error: "Something happened.",
+ })
+ },
+})
+```
+
+The `errorHandler` property's value is a function that accepts four parameters:
+
+1. The error thrown. Its type can be `MedusaError` or any other thrown error type.
+2. A request object of type `MedusaRequest`.
+3. A response object of type `MedusaResponse`.
+4. A function of type MedusaNextFunction that executes the next middleware in the stack.
+
+This example overrides Medusa's default error handler with a handler that always returns a `400` status code with the same message.
+
+
# Handling CORS in API Routes
In this chapter, you’ll learn about the CORS middleware and how to configure it for custom API routes.
@@ -8400,113 +8507,6 @@ export default defineMiddlewares({
This retrieves the configurations exported from `medusa-config.ts` and applies the `storeCors` to routes starting with `/custom`.
-# Throwing and Handling Errors
-
-In this guide, you'll learn how to throw errors in your Medusa application, how it affects an API route's response, and how to change the default error handler of your Medusa application.
-
-## Throw MedusaError
-
-When throwing an error in your API routes, middlewares, workflows, or any customization, throw a `MedusaError` from the Medusa Framework.
-
-The Medusa application's API route error handler then wraps your thrown error in a uniform object and returns it in the response.
-
-For example:
-
-```ts
-import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
-import { MedusaError } from "@medusajs/framework/utils"
-
-export const GET = async (
- req: MedusaRequest,
- res: MedusaResponse
-) => {
- if (!req.query.q) {
- throw new MedusaError(
- MedusaError.Types.INVALID_DATA,
- "The `q` query parameter is required."
- )
- }
-
- // ...
-}
-```
-
-The `MedusaError` class accepts in its constructor two parameters:
-
-1. The first is the error's type. `MedusaError` has a static property `Types` that you can use. `Types` is an enum whose possible values are explained in the next section.
-2. The second is the message to show in the error response.
-
-### Error Object in Response
-
-The error object returned in the response has two properties:
-
-- `type`: The error's type.
-- `message`: The error message, if available.
-- `code`: A common snake-case code. Its values can be:
- - `invalid_request_error` for the `DUPLICATE_ERROR` type.
- - `api_error`: for the `DB_ERROR` type.
- - `invalid_state_error` for `CONFLICT` error type.
- - `unknown_error` for any unidentified error type.
- - For other error types, this property won't be available unless you provide a code as a third parameter to the `MedusaError` constructor.
-
-### MedusaError Types
-
-|Type|Description|Status Code|
-|---|---|---|---|---|
-|\`DB\_ERROR\`|Indicates a database error.|\`500\`|
-|\`DUPLICATE\_ERROR\`|Indicates a duplicate of a record already exists. For example, when trying to create a customer whose email is registered by another customer.|\`422\`|
-|\`INVALID\_ARGUMENT\`|Indicates an error that occurred due to incorrect arguments or other unexpected state.|\`500\`|
-|\`INVALID\_DATA\`|Indicates a validation error.|\`400\`|
-|\`UNAUTHORIZED\`|Indicates that a user is not authorized to perform an action or access a route.|\`401\`|
-|\`NOT\_FOUND\`|Indicates that the requested resource, such as a route or a record, isn't found.|\`404\`|
-|\`NOT\_ALLOWED\`|Indicates that an operation isn't allowed.|\`400\`|
-|\`CONFLICT\`|Indicates that a request conflicts with another previous or ongoing request. The error message in this case is ignored for a default message.|\`409\`|
-|\`PAYMENT\_AUTHORIZATION\_ERROR\`|Indicates an error has occurred while authorizing a payment.|\`422\`|
-|Other error types|Any other error type results in an |\`500\`|
-
-***
-
-## Override Error Handler
-
-The `defineMiddlewares` function used to apply middlewares on routes accepts an `errorHandler` in its object parameter. Use it to override the default error handler for API routes.
-
-This error handler will also be used for errors thrown in Medusa's API routes and resources.
-
-For example, create `src/api/middlewares.ts` with the following:
-
-```ts title="src/api/middlewares.ts" collapsibleLines="1-8" expandMoreLabel="Show Imports"
-import {
- defineMiddlewares,
- MedusaNextFunction,
- MedusaRequest,
- MedusaResponse,
-} from "@medusajs/framework/http"
-import { MedusaError } from "@medusajs/framework/utils"
-
-export default defineMiddlewares({
- errorHandler: (
- error: MedusaError | any,
- req: MedusaRequest,
- res: MedusaResponse,
- next: MedusaNextFunction
- ) => {
- res.status(400).json({
- error: "Something happened.",
- })
- },
-})
-```
-
-The `errorHandler` property's value is a function that accepts four parameters:
-
-1. The error thrown. Its type can be `MedusaError` or any other thrown error type.
-2. A request object of type `MedusaRequest`.
-3. A response object of type `MedusaResponse`.
-4. A function of type MedusaNextFunction that executes the next middleware in the stack.
-
-This example overrides Medusa's default error handler with a handler that always returns a `400` status code with the same message.
-
-
# HTTP Methods
In this chapter, you'll learn about how to add new API routes for each HTTP method.
@@ -8550,6 +8550,151 @@ This adds two API Routes:
- A `POST` route at `http://localhost:9000/hello-world`.
+# API Route Parameters
+
+In this chapter, you’ll learn about path, query, and request body parameters.
+
+## Path Parameters
+
+To create an API route that accepts a path parameter, create a directory within the route file's path whose name is of the format `[param]`.
+
+For example, to create an API Route at the path `/hello-world/:id`, where `:id` is a path parameter, create the file `src/api/hello-world/[id]/route.ts` with the following content:
+
+```ts title="src/api/hello-world/[id]/route.ts" highlights={singlePathHighlights}
+import type {
+ MedusaRequest,
+ MedusaResponse,
+} from "@medusajs/framework/http"
+
+export const GET = async (
+ req: MedusaRequest,
+ res: MedusaResponse
+) => {
+ res.json({
+ message: `[GET] Hello ${req.params.id}!`,
+ })
+}
+```
+
+The `MedusaRequest` object has a `params` property. `params` holds the path parameters in key-value pairs.
+
+### Multiple Path Parameters
+
+To create an API route that accepts multiple path parameters, create within the file's path multiple directories whose names are of the format `[param]`.
+
+For example, to create an API route at `/hello-world/:id/name/:name`, create the file `src/api/hello-world/[id]/name/[name]/route.ts` with the following content:
+
+```ts title="src/api/hello-world/[id]/name/[name]/route.ts" highlights={multiplePathHighlights}
+import type {
+ MedusaRequest,
+ MedusaResponse,
+} from "@medusajs/framework/http"
+
+export const GET = async (
+ req: MedusaRequest,
+ res: MedusaResponse
+) => {
+ res.json({
+ message: `[GET] Hello ${
+ req.params.id
+ } - ${req.params.name}!`,
+ })
+}
+```
+
+You access the `id` and `name` path parameters using the `req.params` property.
+
+***
+
+## Query Parameters
+
+You can access all query parameters in the `query` property of the `MedusaRequest` object. `query` is an object of key-value pairs, where the key is a query parameter's name, and the value is its value.
+
+For example:
+
+```ts title="src/api/hello-world/route.ts" highlights={queryHighlights}
+import type {
+ MedusaRequest,
+ MedusaResponse,
+} from "@medusajs/framework/http"
+
+export const GET = async (
+ req: MedusaRequest,
+ res: MedusaResponse
+) => {
+ res.json({
+ message: `Hello ${req.query.name}`,
+ })
+}
+```
+
+The value of `req.query.name` is the value passed in `?name=John`, for example.
+
+### Validate Query Parameters
+
+You can apply validation rules on received query parameters to ensure they match specified rules and types.
+
+Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation#how-to-validate-request-query-paramters/index.html.md).
+
+***
+
+## Request Body Parameters
+
+The Medusa application parses the body of any request having a JSON, URL-encoded, or text request content types. The request body parameters are set in the `MedusaRequest`'s `body` property.
+
+Learn more about configuring body parsing in [this guide](https://docs.medusajs.com/learn/fundamentals/api-routes/parse-body/index.html.md).
+
+For example:
+
+```ts title="src/api/hello-world/route.ts" highlights={bodyHighlights}
+import type {
+ MedusaRequest,
+ MedusaResponse,
+} from "@medusajs/framework/http"
+
+type HelloWorldReq = {
+ name: string
+}
+
+export const POST = async (
+ req: MedusaRequest,
+ res: MedusaResponse
+) => {
+ res.json({
+ message: `[POST] Hello ${req.body.name}!`,
+ })
+}
+```
+
+In this example, you use the `name` request body parameter to create the message in the returned response.
+
+The `MedusaRequest` type accepts a type argument that indicates the type of the request body. This is useful for auto-completion and to avoid typing errors.
+
+To test it out, send the following request to your Medusa application:
+
+```bash
+curl -X POST 'http://localhost:9000/hello-world' \
+-H 'Content-Type: application/json' \
+--data-raw '{
+ "name": "John"
+}'
+```
+
+This returns the following JSON object:
+
+```json
+{
+ "message": "[POST] Hello John!"
+}
+```
+
+### Validate Body Parameters
+
+You can apply validation rules on received body parameters to ensure they match specified rules and types.
+
+Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation#how-to-validate-request-body/index.html.md).
+
+
# Middlewares
In this chapter, you’ll learn about middlewares and how to create them.
@@ -8981,151 +9126,6 @@ Some examples of when you might want to replicate an API route include:
|Override Middleware|You want to override the middleware applied on existing API routes. Because of |
-# API Route Parameters
-
-In this chapter, you’ll learn about path, query, and request body parameters.
-
-## Path Parameters
-
-To create an API route that accepts a path parameter, create a directory within the route file's path whose name is of the format `[param]`.
-
-For example, to create an API Route at the path `/hello-world/:id`, where `:id` is a path parameter, create the file `src/api/hello-world/[id]/route.ts` with the following content:
-
-```ts title="src/api/hello-world/[id]/route.ts" highlights={singlePathHighlights}
-import type {
- MedusaRequest,
- MedusaResponse,
-} from "@medusajs/framework/http"
-
-export const GET = async (
- req: MedusaRequest,
- res: MedusaResponse
-) => {
- res.json({
- message: `[GET] Hello ${req.params.id}!`,
- })
-}
-```
-
-The `MedusaRequest` object has a `params` property. `params` holds the path parameters in key-value pairs.
-
-### Multiple Path Parameters
-
-To create an API route that accepts multiple path parameters, create within the file's path multiple directories whose names are of the format `[param]`.
-
-For example, to create an API route at `/hello-world/:id/name/:name`, create the file `src/api/hello-world/[id]/name/[name]/route.ts` with the following content:
-
-```ts title="src/api/hello-world/[id]/name/[name]/route.ts" highlights={multiplePathHighlights}
-import type {
- MedusaRequest,
- MedusaResponse,
-} from "@medusajs/framework/http"
-
-export const GET = async (
- req: MedusaRequest,
- res: MedusaResponse
-) => {
- res.json({
- message: `[GET] Hello ${
- req.params.id
- } - ${req.params.name}!`,
- })
-}
-```
-
-You access the `id` and `name` path parameters using the `req.params` property.
-
-***
-
-## Query Parameters
-
-You can access all query parameters in the `query` property of the `MedusaRequest` object. `query` is an object of key-value pairs, where the key is a query parameter's name, and the value is its value.
-
-For example:
-
-```ts title="src/api/hello-world/route.ts" highlights={queryHighlights}
-import type {
- MedusaRequest,
- MedusaResponse,
-} from "@medusajs/framework/http"
-
-export const GET = async (
- req: MedusaRequest,
- res: MedusaResponse
-) => {
- res.json({
- message: `Hello ${req.query.name}`,
- })
-}
-```
-
-The value of `req.query.name` is the value passed in `?name=John`, for example.
-
-### Validate Query Parameters
-
-You can apply validation rules on received query parameters to ensure they match specified rules and types.
-
-Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation#how-to-validate-request-query-paramters/index.html.md).
-
-***
-
-## Request Body Parameters
-
-The Medusa application parses the body of any request having a JSON, URL-encoded, or text request content types. The request body parameters are set in the `MedusaRequest`'s `body` property.
-
-Learn more about configuring body parsing in [this guide](https://docs.medusajs.com/learn/fundamentals/api-routes/parse-body/index.html.md).
-
-For example:
-
-```ts title="src/api/hello-world/route.ts" highlights={bodyHighlights}
-import type {
- MedusaRequest,
- MedusaResponse,
-} from "@medusajs/framework/http"
-
-type HelloWorldReq = {
- name: string
-}
-
-export const POST = async (
- req: MedusaRequest,
- res: MedusaResponse
-) => {
- res.json({
- message: `[POST] Hello ${req.body.name}!`,
- })
-}
-```
-
-In this example, you use the `name` request body parameter to create the message in the returned response.
-
-The `MedusaRequest` type accepts a type argument that indicates the type of the request body. This is useful for auto-completion and to avoid typing errors.
-
-To test it out, send the following request to your Medusa application:
-
-```bash
-curl -X POST 'http://localhost:9000/hello-world' \
--H 'Content-Type: application/json' \
---data-raw '{
- "name": "John"
-}'
-```
-
-This returns the following JSON object:
-
-```json
-{
- "message": "[POST] Hello John!"
-}
-```
-
-### Validate Body Parameters
-
-You can apply validation rules on received body parameters to ensure they match specified rules and types.
-
-Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation#how-to-validate-request-body/index.html.md).
-
-
# Configure Request Body Parser
In this chapter, you'll learn how to configure the request body parser for your API routes.
@@ -9297,6 +9297,88 @@ export async function POST(
Check out the [uploadFilesWorkflow reference](https://docs.medusajs.com/resources/references/medusa-workflows/uploadFilesWorkflow/index.html.md) for details on the expected input and output of the workflow.
+# Retrieve Custom Links from Medusa's API Route
+
+In this chapter, you'll learn how to retrieve custom data models linked to existing Medusa data models from Medusa's API routes.
+
+## Why Retrieve Custom Linked Data Models?
+
+Often, you'll link custom data models to existing Medusa data models to implement custom features or expand on existing ones.
+
+For example, to add brands for products, you can create a `Brand` data model in a Brand Module, then [define a link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) to the [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md)'s `Product` data model.
+
+When you implement this customization, you might need to retrieve the brand of a product using the existing [Get Product API Route](https://docs.medusajs.com/api/admin#products_getproductsid). You can do this by passing the linked data model's name in the `fields` query parameter of the API route.
+
+***
+
+## How to Retrieve Custom Linked Data Models Using `fields`?
+
+Most of Medusa's API routes accept a `fields` query parameter that allows you to specify the fields and relations to retrieve in the resource, such as a product.
+
+For example, to retrieve the brand of a product, you can pass the `brand` field in the `fields` query parameter of the [Get Product API Route](https://docs.medusajs.com/api/admin#products_getproductsid):
+
+```bash
+curl 'http://localhost:9000/admin/products/{id}?fields=*brand' \
+-H 'Authorization: Bearer {access_token}'
+```
+
+The `fields` query parameter accepts a comma-separated list of fields and relations to retrieve. To learn more about using the `fields` query parameter, refer to the [API Reference](https://docs.medusajs.com/api/store#select-fields-and-relations).
+
+By prefixing `brand` with an asterisk (`*`), you retrieve all the default fields of the product, including the `brand` field. If you don't include the `*` prefix, the response will only include the product's brand.
+
+***
+
+## API Routes that Restrict Retrievable Fields
+
+Some of Medusa's API routes restrict the fields and relations you can retrieve, which means you can't pass your custom linked data models in the `fields` query parameter. Medusa makes this restriction to ensure the API routes are performant and secure.
+
+The API routes that restrict the fields and relations you can retrieve are:
+
+- [Customer Store API Routes](https://docs.medusajs.com/api/store#customers)
+- [Customer Admin API Routes](https://docs.medusajs.com/api/admin#customers)
+- [Product Category Admin API Routes](https://docs.medusajs.com/api/admin#product-categories)
+
+### How to Override Allowed Fields and Relations
+
+For these routes, you need to override the allowed fields and relations to be retrieved. You can do this by adding a [middleware](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md) to those routes.
+
+For example, to allow retrieving the `b2b_company` of a customer using the [Get Customer Admin API Route](https://docs.medusajs.com/api/admin#customers_getcustomersid), create the file `src/api/middlewares.ts` with the following content:
+
+Learn how to create a middleware in the [Middlewares](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md) chapter.
+
+```ts title="src/api/middlewares.ts" highlights={highlights}
+import { defineMiddlewares } from "@medusajs/medusa"
+
+export default defineMiddlewares({
+ routes: [
+ {
+ matcher: "/store/customers/me",
+ method: "GET",
+ middlewares: [
+ (req, res, next) => {
+ req.allowed?.push("b2b_company")
+ next()
+ },
+ ],
+ },
+ ],
+})
+```
+
+In this example, you apply a middleware to the [Get Customer Admin API Route](https://docs.medusajs.com/api/admin#customers_getcustomersid).
+
+The request object passed to middlewares has an `allowed` property that contains the fields and relations that can be retrieved. So, you modify the `allowed` array to include the `b2b_company` field.
+
+You can now retrieve the `b2b_company` field using the `fields` query parameter of the [Get Customer Admin API Route](https://docs.medusajs.com/api/admin#customers_getcustomersid):
+
+```bash
+curl 'http://localhost:9000/admin/customers/{id}?fields=*b2b_company' \
+-H 'Authorization: Bearer {access_token}'
+```
+
+In this example, you retrieve the `b2b_company` relation of the customer using the `fields` query parameter.
+
+
# Protected API Routes
In this chapter, you’ll learn how to create protected API routes.
@@ -9664,88 +9746,6 @@ This API route opens a stream by setting the `Content-Type` in the header to `te
The `MedusaResponse` type is based on [Express's Response](https://expressjs.com/en/api.html#res). Refer to their API reference for other uses of responses.
-# Retrieve Custom Links from Medusa's API Route
-
-In this chapter, you'll learn how to retrieve custom data models linked to existing Medusa data models from Medusa's API routes.
-
-## Why Retrieve Custom Linked Data Models?
-
-Often, you'll link custom data models to existing Medusa data models to implement custom features or expand on existing ones.
-
-For example, to add brands for products, you can create a `Brand` data model in a Brand Module, then [define a link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) to the [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md)'s `Product` data model.
-
-When you implement this customization, you might need to retrieve the brand of a product using the existing [Get Product API Route](https://docs.medusajs.com/api/admin#products_getproductsid). You can do this by passing the linked data model's name in the `fields` query parameter of the API route.
-
-***
-
-## How to Retrieve Custom Linked Data Models Using `fields`?
-
-Most of Medusa's API routes accept a `fields` query parameter that allows you to specify the fields and relations to retrieve in the resource, such as a product.
-
-For example, to retrieve the brand of a product, you can pass the `brand` field in the `fields` query parameter of the [Get Product API Route](https://docs.medusajs.com/api/admin#products_getproductsid):
-
-```bash
-curl 'http://localhost:9000/admin/products/{id}?fields=*brand' \
--H 'Authorization: Bearer {access_token}'
-```
-
-The `fields` query parameter accepts a comma-separated list of fields and relations to retrieve. To learn more about using the `fields` query parameter, refer to the [API Reference](https://docs.medusajs.com/api/store#select-fields-and-relations).
-
-By prefixing `brand` with an asterisk (`*`), you retrieve all the default fields of the product, including the `brand` field. If you don't include the `*` prefix, the response will only include the product's brand.
-
-***
-
-## API Routes that Restrict Retrievable Fields
-
-Some of Medusa's API routes restrict the fields and relations you can retrieve, which means you can't pass your custom linked data models in the `fields` query parameter. Medusa makes this restriction to ensure the API routes are performant and secure.
-
-The API routes that restrict the fields and relations you can retrieve are:
-
-- [Customer Store API Routes](https://docs.medusajs.com/api/store#customers)
-- [Customer Admin API Routes](https://docs.medusajs.com/api/admin#customers)
-- [Product Category Admin API Routes](https://docs.medusajs.com/api/admin#product-categories)
-
-### How to Override Allowed Fields and Relations
-
-For these routes, you need to override the allowed fields and relations to be retrieved. You can do this by adding a [middleware](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md) to those routes.
-
-For example, to allow retrieving the `b2b_company` of a customer using the [Get Customer Admin API Route](https://docs.medusajs.com/api/admin#customers_getcustomersid), create the file `src/api/middlewares.ts` with the following content:
-
-Learn how to create a middleware in the [Middlewares](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md) chapter.
-
-```ts title="src/api/middlewares.ts" highlights={highlights}
-import { defineMiddlewares } from "@medusajs/medusa"
-
-export default defineMiddlewares({
- routes: [
- {
- matcher: "/store/customers/me",
- method: "GET",
- middlewares: [
- (req, res, next) => {
- req.allowed?.push("b2b_company")
- next()
- },
- ],
- },
- ],
-})
-```
-
-In this example, you apply a middleware to the [Get Customer Admin API Route](https://docs.medusajs.com/api/admin#customers_getcustomersid).
-
-The request object passed to middlewares has an `allowed` property that contains the fields and relations that can be retrieved. So, you modify the `allowed` array to include the `b2b_company` field.
-
-You can now retrieve the `b2b_company` field using the `fields` query parameter of the [Get Customer Admin API Route](https://docs.medusajs.com/api/admin#customers_getcustomersid):
-
-```bash
-curl 'http://localhost:9000/admin/customers/{id}?fields=*b2b_company' \
--H 'Authorization: Bearer {access_token}'
-```
-
-In this example, you retrieve the `b2b_company` relation of the customer using the `fields` query parameter.
-
-
# Request Body and Query Parameter Validation
In this chapter, you'll learn how to validate request body and query parameters in your custom API route.
@@ -9995,6 +9995,51 @@ 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. */}
+
+
# Add Data Model Check Constraints
In this chapter, you'll learn how to add check constraints to your data model.
@@ -10067,6 +10112,174 @@ 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.
+# Emit Workflow and Service Events
+
+In this chapter, you'll learn about event types and how to emit an event in a service or workflow.
+
+## Event Types
+
+In your customization, you can emit an event, then listen to it in a subscriber and perform an asynchronus action, such as send a notification or data to a third-party system.
+
+There are two types of events in Medusa:
+
+1. Workflow event: an event that's emitted in a workflow after a commerce feature is performed. For example, Medusa emits the `order.placed` event after a cart is completed.
+2. Service event: an event that's emitted to track, trace, or debug processes under the hood. For example, you can emit an event with an audit trail.
+
+### Which Event Type to Use?
+
+**Workflow events** are the most common event type in development, as most custom features and customizations are built around workflows.
+
+Some examples of workflow events:
+
+1. When a user creates a blog post and you're emitting an event to send a newsletter email.
+2. When you finish syncing products to a third-party system and you want to notify the admin user of new products added.
+3. When a customer purchases a digital product and you want to generate and send it to them.
+
+You should only go for a **service event** if you're emitting an event for processes under the hood that don't directly affect front-facing features.
+
+Some examples of service events:
+
+1. When you're tracing data manipulation and changes, and you want to track every time some custom data is changed.
+2. When you're syncing data with a search engine.
+
+***
+
+## Emit Event in a Workflow
+
+To emit a workflow event, use the `emitEventStep` helper step provided in the `@medusajs/medusa/core-flows` package.
+
+For example:
+
+```ts highlights={highlights}
+import {
+ createWorkflow,
+} from "@medusajs/framework/workflows-sdk"
+import {
+ emitEventStep,
+} from "@medusajs/medusa/core-flows"
+
+const helloWorldWorkflow = createWorkflow(
+ "hello-world",
+ () => {
+ // ...
+
+ emitEventStep({
+ eventName: "custom.created",
+ data: {
+ id: "123",
+ // other data payload
+ },
+ })
+ }
+)
+```
+
+The `emitEventStep` accepts an object having the following properties:
+
+- `eventName`: The event's name.
+- `data`: The data payload as an object. You can pass any properties in the object, and subscribers listening to the event will receive this data in the event's payload.
+
+In this example, you emit the event `custom.created` and pass in the data payload an ID property.
+
+### Test it Out
+
+If you execute the workflow, the event is emitted and you can see it in your application's logs.
+
+Any subscribers listening to the event are executed.
+
+***
+
+## Emit Event in a Service
+
+To emit a service event:
+
+1. Resolve `event_bus` from the module's container in your service's constructor:
+
+### Extending Service Factory
+
+```ts title="src/modules/blog/service.ts" highlights={["9"]}
+import { IEventBusService } from "@medusajs/framework/types"
+import { MedusaService } from "@medusajs/framework/utils"
+
+class BlogModuleService extends MedusaService({
+ Post,
+}){
+ protected eventBusService_: AbstractEventBusModuleService
+
+ constructor({ event_bus }) {
+ super(...arguments)
+ this.eventBusService_ = event_bus
+ }
+}
+```
+
+### Without Service Factory
+
+```ts title="src/modules/blog/service.ts" highlights={["6"]}
+import { IEventBusService } from "@medusajs/framework/types"
+
+class BlogModuleService {
+ protected eventBusService_: AbstractEventBusModuleService
+
+ constructor({ event_bus }) {
+ this.eventBusService_ = event_bus
+ }
+}
+```
+
+2. Use the event bus service's `emit` method in the service's methods to emit an event:
+
+```ts title="src/modules/blog/service.ts" highlights={serviceHighlights}
+class BlogModuleService {
+ // ...
+ performAction() {
+ // TODO perform action
+
+ this.eventBusService_.emit({
+ name: "custom.event",
+ data: {
+ id: "123",
+ // other data payload
+ },
+ })
+ }
+}
+```
+
+The method accepts an object having the following properties:
+
+- `name`: The event's name.
+- `data`: The data payload as an object. You can pass any properties in the object, and subscribers listening to the event will receive this data in the event's payload.
+
+3. By default, the Event Module's service isn't injected into your module's container. To add it to the container, pass it in the module's registration object in `medusa-config.ts` in the `dependencies` property:
+
+```ts title="medusa-config.ts" highlights={depsHighlight}
+import { Modules } from "@medusajs/framework/utils"
+
+module.exports = defineConfig({
+ // ...
+ modules: [
+ {
+ resolve: "./src/modules/blog",
+ dependencies: [
+ Modules.EVENT_BUS,
+ ],
+ },
+ ],
+})
+```
+
+The `dependencies` property accepts an array of module registration keys. The specified modules' main services are injected into the module's container.
+
+That's how you can resolve it in your module's main service's constructor.
+
+### Test it Out
+
+If you execute the `performAction` method of your service, the event is emitted and you can see it in your application's logs.
+
+Any subscribers listening to the event are also executed.
+
+
# Data Model Database Index
In this chapter, you’ll learn how to define a database index on a data model.
@@ -11086,317 +11299,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).
-
-
-# Event Data Payload
-
-In this chapter, you'll learn how subscribers receive an event's data payload.
-
-## Access Event's Data Payload
-
-When events are emitted, they’re emitted with a data payload.
-
-The object that the subscriber function receives as a parameter has an `event` property, which is an object holding the event payload in a `data` property with additional context.
-
-For example:
-
-```ts title="src/subscribers/product-created.ts" highlights={highlights} collapsibleLines="1-5" expandButtonLabel="Show Imports"
-import type {
- SubscriberArgs,
- SubscriberConfig,
-} from "@medusajs/framework"
-
-export default async function productCreateHandler({
- event,
-}: SubscriberArgs<{ id: string }>) {
- const productId = event.data.id
- console.log(`The product ${productId} was created`)
-}
-
-export const config: SubscriberConfig = {
- event: "product.created",
-}
-```
-
-The `event` object has the following properties:
-
-- data: (\`object\`) The data payload of the event. Its properties are different for each event.
-- name: (string) The name of the triggered event.
-- metadata: (\`object\`) Additional data and context of the emitted event.
-
-This logs the product ID received in the `product.created` event’s data payload to the console.
-
-{/* ---
-
-## List of Events with Data Payload
-
-Refer to [this reference](!resources!/references/events) for a full list of events emitted by Medusa and their data payloads. */}
-
-
-# Emit Workflow and Service Events
-
-In this chapter, you'll learn about event types and how to emit an event in a service or workflow.
-
-## Event Types
-
-In your customization, you can emit an event, then listen to it in a subscriber and perform an asynchronus action, such as send a notification or data to a third-party system.
-
-There are two types of events in Medusa:
-
-1. Workflow event: an event that's emitted in a workflow after a commerce feature is performed. For example, Medusa emits the `order.placed` event after a cart is completed.
-2. Service event: an event that's emitted to track, trace, or debug processes under the hood. For example, you can emit an event with an audit trail.
-
-### Which Event Type to Use?
-
-**Workflow events** are the most common event type in development, as most custom features and customizations are built around workflows.
-
-Some examples of workflow events:
-
-1. When a user creates a blog post and you're emitting an event to send a newsletter email.
-2. When you finish syncing products to a third-party system and you want to notify the admin user of new products added.
-3. When a customer purchases a digital product and you want to generate and send it to them.
-
-You should only go for a **service event** if you're emitting an event for processes under the hood that don't directly affect front-facing features.
-
-Some examples of service events:
-
-1. When you're tracing data manipulation and changes, and you want to track every time some custom data is changed.
-2. When you're syncing data with a search engine.
-
-***
-
-## Emit Event in a Workflow
-
-To emit a workflow event, use the `emitEventStep` helper step provided in the `@medusajs/medusa/core-flows` package.
-
-For example:
-
-```ts highlights={highlights}
-import {
- createWorkflow,
-} from "@medusajs/framework/workflows-sdk"
-import {
- emitEventStep,
-} from "@medusajs/medusa/core-flows"
-
-const helloWorldWorkflow = createWorkflow(
- "hello-world",
- () => {
- // ...
-
- emitEventStep({
- eventName: "custom.created",
- data: {
- id: "123",
- // other data payload
- },
- })
- }
-)
-```
-
-The `emitEventStep` accepts an object having the following properties:
-
-- `eventName`: The event's name.
-- `data`: The data payload as an object. You can pass any properties in the object, and subscribers listening to the event will receive this data in the event's payload.
-
-In this example, you emit the event `custom.created` and pass in the data payload an ID property.
-
-### Test it Out
-
-If you execute the workflow, the event is emitted and you can see it in your application's logs.
-
-Any subscribers listening to the event are executed.
-
-***
-
-## Emit Event in a Service
-
-To emit a service event:
-
-1. Resolve `event_bus` from the module's container in your service's constructor:
-
-### Extending Service Factory
-
-```ts title="src/modules/blog/service.ts" highlights={["9"]}
-import { IEventBusService } from "@medusajs/framework/types"
-import { MedusaService } from "@medusajs/framework/utils"
-
-class BlogModuleService extends MedusaService({
- Post,
-}){
- protected eventBusService_: AbstractEventBusModuleService
-
- constructor({ event_bus }) {
- super(...arguments)
- this.eventBusService_ = event_bus
- }
-}
-```
-
-### Without Service Factory
-
-```ts title="src/modules/blog/service.ts" highlights={["6"]}
-import { IEventBusService } from "@medusajs/framework/types"
-
-class BlogModuleService {
- protected eventBusService_: AbstractEventBusModuleService
-
- constructor({ event_bus }) {
- this.eventBusService_ = event_bus
- }
-}
-```
-
-2. Use the event bus service's `emit` method in the service's methods to emit an event:
-
-```ts title="src/modules/blog/service.ts" highlights={serviceHighlights}
-class BlogModuleService {
- // ...
- performAction() {
- // TODO perform action
-
- this.eventBusService_.emit({
- name: "custom.event",
- data: {
- id: "123",
- // other data payload
- },
- })
- }
-}
-```
-
-The method accepts an object having the following properties:
-
-- `name`: The event's name.
-- `data`: The data payload as an object. You can pass any properties in the object, and subscribers listening to the event will receive this data in the event's payload.
-
-3. By default, the Event Module's service isn't injected into your module's container. To add it to the container, pass it in the module's registration object in `medusa-config.ts` in the `dependencies` property:
-
-```ts title="medusa-config.ts" highlights={depsHighlight}
-import { Modules } from "@medusajs/framework/utils"
-
-module.exports = defineConfig({
- // ...
- modules: [
- {
- resolve: "./src/modules/blog",
- dependencies: [
- Modules.EVENT_BUS,
- ],
- },
- ],
-})
-```
-
-The `dependencies` property accepts an array of module registration keys. The specified modules' main services are injected into the module's container.
-
-That's how you can resolve it in your module's main service's constructor.
-
-### Test it Out
-
-If you execute the `performAction` method of your service, the event is emitted and you can see it in your application's logs.
-
-Any subscribers listening to the event are also executed.
-
-
# Module Link Direction
In this chapter, you'll learn about the difference in module link directions, and which to use based on your use case.
@@ -11616,6 +11518,104 @@ await link.create({
```
+# 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).
+
+
# Link
In this chapter, you’ll learn what Link is and how to use it to manage links.
@@ -11820,562 +11820,6 @@ await link.restore({
```
-# Query
-
-In this chapter, you’ll learn about Query and how to use it to fetch data from modules.
-
-## What is Query?
-
-Query fetches data across modules. It’s a set of methods registered in the Medusa container under the `query` key.
-
-In all resources that can access the [Medusa Container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md), such as API routes or workflows, you can resolve Query to fetch data across custom modules and Medusa’s Commerce Modules.
-
-***
-
-## Query Example
-
-For example, create the route `src/api/query/route.ts` with the following content:
-
-```ts title="src/api/query/route.ts" highlights={exampleHighlights} collapsibleLines="1-8" expandButtonLabel="Show Imports"
-import {
- MedusaRequest,
- MedusaResponse,
-} from "@medusajs/framework/http"
-import {
- ContainerRegistrationKeys,
-} from "@medusajs/framework/utils"
-
-export const GET = async (
- req: MedusaRequest,
- res: MedusaResponse
-) => {
- const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
-
- const { data: posts } = await query.graph({
- entity: "post",
- fields: ["id", "title"],
- })
-
- res.json({ posts })
-}
-```
-
-In the above example, you resolve Query from the Medusa container using the `ContainerRegistrationKeys.QUERY` (`query`) key.
-
-Then, you run a query using its `graph` method. This method accepts as a parameter an object with the following required properties:
-
-- `entity`: The data model's name, as specified in the first parameter of the `model.define` method used for the data model's definition.
-- `fields`: An array of the data model’s properties to retrieve in the result.
-
-The method returns an object that has a `data` property, which holds an array of the retrieved data. For example:
-
-```json title="Returned Data"
-{
- "data": [
- {
- "id": "123",
- "title": "My Post"
- }
- ]
-}
-```
-
-***
-
-## Querying the Graph
-
-When you use the `query.graph` method, you're running a query through an internal graph that the Medusa application creates.
-
-This graph collects data models of all modules in your application, including commerce and custom modules, and identifies relations and links between them.
-
-***
-
-## Retrieve Linked Records
-
-Retrieve the records of a linked data model by passing in `fields` the data model's name suffixed with `.*`.
-
-For example:
-
-```ts highlights={[["6"]]}
-const { data: posts } = await query.graph({
- entity: "post",
- fields: [
- "id",
- "title",
- "product.*",
- ],
-})
-```
-
-`.*` means that all of data model's properties should be retrieved. You can also retrieve specific properties by replacing the `*` with the property name, for each property.
-
-For example:
-
-```ts
-const { data: posts } = await query.graph({
- entity: "post",
- fields: [
- "id",
- "title",
- "product.id",
- "product.title",
- ],
-})
-```
-
-In the example above, you retrieve only the `id` and `title` properties of the `product` linked to a `post`.
-
-### Retrieve List Link Records
-
-If the linked data model has `isList` enabled in the link definition, pass in `fields` the data model's plural name suffixed with `.*`.
-
-For example:
-
-```ts highlights={[["6"]]}
-const { data: posts } = await query.graph({
- entity: "post",
- fields: [
- "id",
- "title",
- "products.*",
- ],
-})
-```
-
-In the example above, you retrieve all products linked to a post.
-
-### Apply Filters and Pagination on Linked Records
-
-Consider you want to apply filters or pagination configurations on the product(s) linked to `post`. To do that, you must query the module link's table instead.
-
-As mentioned in the [Module Link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) documentation, Medusa creates a table for your module link. So, not only can you retrieve linked records, but you can also retrieve the records in a module link's table.
-
-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={queryLinkTableHighlights}
-import ProductPostLink from "../../../links/product-post"
-
-// ...
-
-const { data: productCustoms } = await query.graph({
- entity: ProductPostLink.entryPoint,
- fields: ["*", "product.*", "post.*"],
- pagination: {
- take: 5,
- skip: 0,
- },
-})
-```
-
-In the object passed to the `graph` method:
-
-- You pass the `entryPoint` property of the link definition as the value for `entity`. So, Query will retrieve records from the module link's table.
-- You pass three items to the `field` property:
- - `*` to retrieve the link table's fields. This is useful if the link table has [custom columns](https://docs.medusajs.com/learn/fundamentals/module-links/custom-columns/index.html.md).
- - `product.*` to retrieve the fields of a product record linked to a `Post` record.
- - `post.*` to retrieve the fields of a `Post` record linked to a product record.
-
-You can then apply any [filters](#apply-filters) or [pagination configurations](#apply-pagination) on the module link's table. For example, you can apply filters on the `product_id`, `post_id`, and any other custom columns you defined in the link table.
-
-The returned `data` is similar to the following:
-
-```json title="Example Result"
-[{
- "id": "123",
- "product_id": "prod_123",
- "post_id": "123",
- "product": {
- "id": "prod_123",
- // other product fields...
- },
- "post": {
- "id": "123",
- // other post fields...
- }
-}]
-```
-
-***
-
-## Apply Filters
-
-```ts highlights={[["4"], ["5"], ["6"]]}
-const { data: posts } = await query.graph({
- entity: "post",
- fields: ["id", "title"],
- filters: {
- id: "post_123",
- },
-})
-```
-
-The `query.graph` function accepts a `filters` property. You can use this property to filter retrieved records.
-
-In the example above, you filter the `post` records by the ID `post_123`.
-
-You can also filter by multiple values of a property. For example:
-
-```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"], ["9"]]}
-const { data: posts } = await query.graph({
- entity: "post",
- fields: ["id", "title"],
- filters: {
- id: [
- "post_123",
- "post_321",
- ],
- },
-})
-```
-
-In the example above, you filter the `post` records by multiple IDs.
-
-Filters don't apply on fields of linked data models from other modules. Refer to the [Retrieve Linked Records](#retrieve-linked-records) section for an alternative solution.
-
-### Advanced Query Filters
-
-Under the hood, Query uses one of the following methods from the data model's module's service to retrieve records:
-
-- `listX` if you don't pass [pagination parameters](#apply-pagination). For example, `listPosts`.
-- `listAndCountX` if you pass pagination parameters. For example, `listAndCountPosts`.
-
-Both methods accepts a filter object that can be used to filter records.
-
-Those filters don't just allow you to filter by exact values. You can also filter by properties that don't match a value, match multiple values, and other filter types.
-
-Refer to the [Service Factory Reference](https://docs.medusajs.com/resources/service-factory-reference/tips/filtering/index.html.md) for examples of advanced filters. The following sections provide some quick examples.
-
-#### Filter by Not Matching a Value
-
-```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"]]}
-const { data: posts } = await query.graph({
- entity: "post",
- fields: ["id", "title"],
- filters: {
- title: {
- $ne: null,
- },
- },
-})
-```
-
-In the example above, only posts that have a title are retrieved.
-
-#### Filter by Not Matching Multiple Values
-
-```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"]]}
-const { data: posts } = await query.graph({
- entity: "post",
- fields: ["id", "title"],
- filters: {
- title: {
- $nin: ["My Post", "Another Post"],
- },
- },
-})
-```
-
-In the example above, only posts that don't have the title `My Post` or `Another Post` are retrieved.
-
-#### Filter by a Range
-
-```ts highlights={[["10"], ["11"], ["12"], ["13"], ["14"], ["15"]]}
-const startToday = new Date()
-startToday.setHours(0, 0, 0, 0)
-
-const endToday = new Date()
-endToday.setHours(23, 59, 59, 59)
-
-const { data: posts } = await query.graph({
- entity: "post",
- fields: ["id", "title"],
- filters: {
- published_at: {
- $gt: startToday,
- $lt: endToday,
- },
- },
-})
-```
-
-In the example above, only posts that were published today are retrieved.
-
-#### Filter Text by Like Value
-
-This filter only applies to text-like properties, including `text`, `id`, and `enum` properties.
-
-```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"]]}
-const { data: posts } = await query.graph({
- entity: "post",
- fields: ["id", "title"],
- filters: {
- title: {
- $like: "%My%",
- },
- },
-})
-```
-
-In the example above, only posts that have the word `My` in their title are retrieved.
-
-#### Filter a Relation's Property
-
-```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"]]}
-const { data: posts } = await query.graph({
- entity: "post",
- fields: ["id", "title"],
- filters: {
- author: {
- name: "John",
- },
- },
-})
-```
-
-While it's not possible to filter by a linked data model's property, you can filter by a relation's property (that is, the property of a related data model that is defined in the same module).
-
-In the example above, only posts that have an author with the name `John` are retrieved.
-
-***
-
-## Apply Pagination
-
-```ts highlights={[["8", "skip", "The number of records to skip before fetching the results."], ["9", "take", "The number of records to fetch."]]}
-const {
- data: posts,
- metadata: { count, take, skip } = {},
-} = await query.graph({
- entity: "post",
- fields: ["id", "title"],
- pagination: {
- skip: 0,
- take: 10,
- },
-})
-```
-
-The `graph` method's object parameter accepts a `pagination` property to configure the pagination of retrieved records.
-
-To paginate the returned records, pass the following properties to `pagination`:
-
-- `skip`: (required to apply pagination) The number of records to skip before fetching the results.
-- `take`: The number of records to fetch.
-
-When you provide the pagination fields, the `query.graph` method's returned object has a `metadata` property. Its value is an object having the following properties:
-
-- skip: (\`number\`) The number of records skipped.
-- take: (\`number\`) The number of records requested to fetch.
-- count: (\`number\`) The total number of records.
-
-### Sort Records
-
-```ts highlights={[["5"], ["6"], ["7"]]}
-const { data: posts } = await query.graph({
- entity: "post",
- fields: ["id", "title"],
- pagination: {
- order: {
- name: "DESC",
- },
- },
-})
-```
-
-Sorting doesn't work on fields of linked data models from other modules.
-
-To sort returned records, pass an `order` property to `pagination`.
-
-The `order` property is an object whose keys are property names, and values are either:
-
-- `ASC` to sort records by that property in ascending order.
-- `DESC` to sort records by that property in descending order.
-
-***
-
-## Configure Query to Throw Errors
-
-By default, if Query doesn't find records matching your query, it returns an empty array. You can add option to configure Query to throw an error when no records are found.
-
-The `query.graph` method accepts as a second parameter an object that can have a `throwIfKeyNotFound` property. Its value is a boolean indicating whether to throw an error if no record is found when filtering by IDs. By default, it's `false`.
-
-For example:
-
-```ts
-const { data: posts } = await query.graph({
- entity: "post",
- fields: ["id", "title"],
- filters: {
- id: "post_123",
- },
-}, {
- throwIfKeyNotFound: true,
-})
-```
-
-In the example above, if no post is found with the ID `post_123`, Query will throw an error. This is useful to stop execution when a record is expected to exist.
-
-### Throw Error on Related Data Model
-
-The `throwIfKeyNotFound` option can also be used to throw an error if the ID of a related data model's record (in the same module) is passed in the filters, and the related record doesn't exist.
-
-For example:
-
-```ts
-const { data: posts } = await query.graph({
- entity: "post",
- fields: ["id", "title", "author.*"],
- filters: {
- id: "post_123",
- author_id: "author_123",
- },
-}, {
- throwIfKeyNotFound: true,
-})
-```
-
-In the example above, Query throws an error either if no post is found with the ID `post_123` or if its found but its author ID isn't `author_123`.
-
-In the above example, it's assumed that a post belongs to an author, so it has an `author_id` property. However, this also works in the opposite case, where an author has many posts.
-
-For example:
-
-```ts
-const { data: posts } = await query.graph({
- entity: "author",
- fields: ["id", "name", "posts.*"],
- filters: {
- id: "author_123",
- posts: {
- id: "post_123",
- },
- },
-}, {
- throwIfKeyNotFound: true,
-})
-```
-
-In the example above, Query throws an error if no author is found with the ID `author_123` or if the author is found but doesn't have a post with the ID `post_123`.
-
-***
-
-## Request Query Configurations
-
-For API routes that retrieve a single or list of resources, Medusa provides a `validateAndTransformQuery` middleware that:
-
-- Validates accepted query parameters, as explained in [this documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation/index.html.md).
-- Parses configurations that are received as query parameters to be passed to Query.
-
-Using this middleware allows you to have default configurations for retrieved fields and relations or pagination, while allowing clients to customize them per request.
-
-### Step 1: Add Middleware
-
-The first step is to use the `validateAndTransformQuery` middleware on the `GET` route. You add the middleware in `src/api/middlewares.ts`:
-
-```ts title="src/api/middlewares.ts"
-import {
- validateAndTransformQuery,
- defineMiddlewares,
-} from "@medusajs/framework/http"
-import { createFindParams } from "@medusajs/medusa/api/utils/validators"
-
-export const GetCustomSchema = createFindParams()
-
-export default defineMiddlewares({
- routes: [
- {
- matcher: "/customs",
- method: "GET",
- middlewares: [
- validateAndTransformQuery(
- GetCustomSchema,
- {
- defaults: [
- "id",
- "title",
- "products.*",
- ],
- isList: true,
- }
- ),
- ],
- },
- ],
-})
-```
-
-The `validateAndTransformQuery` accepts two parameters:
-
-1. A Zod validation schema for the query parameters, which you can learn more about in the [API Route Validation documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation/index.html.md). Medusa has a `createFindParams` utility that generates a Zod schema that accepts four query parameters:
- 1. `fields`: The fields and relations to retrieve in the returned resources.
- 2. `offset`: The number of items to skip before retrieving the returned items.
- 3. `limit`: The maximum number of items to return.
- 4. `order`: The fields to order the returned items by in ascending or descending order.
-2. A Query configuration object. It accepts the following properties:
- 1. `defaults`: An array of default fields and relations to retrieve in each resource.
- 2. `isList`: A boolean indicating whether a list of items are returned in the response.
- 3. `allowed`: An array of fields and relations allowed to be passed in the `fields` query parameter.
- 4. `defaultLimit`: A number indicating the default limit to use if no limit is provided. By default, it's `50`.
-
-### Step 2: Use Configurations in API Route
-
-After applying this middleware, your API route now accepts the `fields`, `offset`, `limit`, and `order` query parameters mentioned above.
-
-The middleware transforms these parameters to configurations that you can pass to Query in your API route handler. These configurations are stored in the `queryConfig` parameter of the `MedusaRequest` object.
-
-As of [Medusa v2.2.0](https://github.com/medusajs/medusa/releases/tag/v2.2.0), `remoteQueryConfig` has been deprecated in favor of `queryConfig`. Their usage is still the same, only the property name has changed.
-
-For example, Create the file `src/api/customs/route.ts` with the following content:
-
-```ts title="src/api/customs/route.ts"
-import {
- MedusaRequest,
- MedusaResponse,
-} from "@medusajs/framework/http"
-import {
- ContainerRegistrationKeys,
-} from "@medusajs/framework/utils"
-
-export const GET = async (
- req: MedusaRequest,
- res: MedusaResponse
-) => {
- const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
-
- const { data: posts } = await query.graph({
- entity: "post",
- ...req.queryConfig,
- })
-
- res.json({ posts: posts })
-}
-```
-
-This adds a `GET` API route at `/customs`, which is the API route you added the middleware for.
-
-In the API route, you pass `req.queryConfig` to `query.graph`. `queryConfig` has properties like `fields` and `pagination` to configure the query based on the default values you specified in the middleware, and the query parameters passed in the request.
-
-### Test it Out
-
-To test it out, start your Medusa application and send a `GET` request to the `/customs` API route. A list of records are retrieved with the specified fields in the middleware.
-
-```json title="Returned Data"
-{
- "posts": [
- {
- "id": "123",
- "title": "test"
- }
- ]
-}
-```
-
-Try passing one of the Query configuration parameters, like `fields` or `limit`, and you'll see its impact on the returned result.
-
-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).
@@ -13108,6 +12552,562 @@ 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
+
+In this chapter, you’ll learn about Query and how to use it to fetch data from modules.
+
+## What is Query?
+
+Query fetches data across modules. It’s a set of methods registered in the Medusa container under the `query` key.
+
+In all resources that can access the [Medusa Container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md), such as API routes or workflows, you can resolve Query to fetch data across custom modules and Medusa’s Commerce Modules.
+
+***
+
+## Query Example
+
+For example, create the route `src/api/query/route.ts` with the following content:
+
+```ts title="src/api/query/route.ts" highlights={exampleHighlights} collapsibleLines="1-8" expandButtonLabel="Show Imports"
+import {
+ MedusaRequest,
+ MedusaResponse,
+} from "@medusajs/framework/http"
+import {
+ ContainerRegistrationKeys,
+} from "@medusajs/framework/utils"
+
+export const GET = async (
+ req: MedusaRequest,
+ res: MedusaResponse
+) => {
+ const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
+
+ const { data: posts } = await query.graph({
+ entity: "post",
+ fields: ["id", "title"],
+ })
+
+ res.json({ posts })
+}
+```
+
+In the above example, you resolve Query from the Medusa container using the `ContainerRegistrationKeys.QUERY` (`query`) key.
+
+Then, you run a query using its `graph` method. This method accepts as a parameter an object with the following required properties:
+
+- `entity`: The data model's name, as specified in the first parameter of the `model.define` method used for the data model's definition.
+- `fields`: An array of the data model’s properties to retrieve in the result.
+
+The method returns an object that has a `data` property, which holds an array of the retrieved data. For example:
+
+```json title="Returned Data"
+{
+ "data": [
+ {
+ "id": "123",
+ "title": "My Post"
+ }
+ ]
+}
+```
+
+***
+
+## Querying the Graph
+
+When you use the `query.graph` method, you're running a query through an internal graph that the Medusa application creates.
+
+This graph collects data models of all modules in your application, including commerce and custom modules, and identifies relations and links between them.
+
+***
+
+## Retrieve Linked Records
+
+Retrieve the records of a linked data model by passing in `fields` the data model's name suffixed with `.*`.
+
+For example:
+
+```ts highlights={[["6"]]}
+const { data: posts } = await query.graph({
+ entity: "post",
+ fields: [
+ "id",
+ "title",
+ "product.*",
+ ],
+})
+```
+
+`.*` means that all of data model's properties should be retrieved. You can also retrieve specific properties by replacing the `*` with the property name, for each property.
+
+For example:
+
+```ts
+const { data: posts } = await query.graph({
+ entity: "post",
+ fields: [
+ "id",
+ "title",
+ "product.id",
+ "product.title",
+ ],
+})
+```
+
+In the example above, you retrieve only the `id` and `title` properties of the `product` linked to a `post`.
+
+### Retrieve List Link Records
+
+If the linked data model has `isList` enabled in the link definition, pass in `fields` the data model's plural name suffixed with `.*`.
+
+For example:
+
+```ts highlights={[["6"]]}
+const { data: posts } = await query.graph({
+ entity: "post",
+ fields: [
+ "id",
+ "title",
+ "products.*",
+ ],
+})
+```
+
+In the example above, you retrieve all products linked to a post.
+
+### Apply Filters and Pagination on Linked Records
+
+Consider you want to apply filters or pagination configurations on the product(s) linked to `post`. To do that, you must query the module link's table instead.
+
+As mentioned in the [Module Link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) documentation, Medusa creates a table for your module link. So, not only can you retrieve linked records, but you can also retrieve the records in a module link's table.
+
+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={queryLinkTableHighlights}
+import ProductPostLink from "../../../links/product-post"
+
+// ...
+
+const { data: productCustoms } = await query.graph({
+ entity: ProductPostLink.entryPoint,
+ fields: ["*", "product.*", "post.*"],
+ pagination: {
+ take: 5,
+ skip: 0,
+ },
+})
+```
+
+In the object passed to the `graph` method:
+
+- You pass the `entryPoint` property of the link definition as the value for `entity`. So, Query will retrieve records from the module link's table.
+- You pass three items to the `field` property:
+ - `*` to retrieve the link table's fields. This is useful if the link table has [custom columns](https://docs.medusajs.com/learn/fundamentals/module-links/custom-columns/index.html.md).
+ - `product.*` to retrieve the fields of a product record linked to a `Post` record.
+ - `post.*` to retrieve the fields of a `Post` record linked to a product record.
+
+You can then apply any [filters](#apply-filters) or [pagination configurations](#apply-pagination) on the module link's table. For example, you can apply filters on the `product_id`, `post_id`, and any other custom columns you defined in the link table.
+
+The returned `data` is similar to the following:
+
+```json title="Example Result"
+[{
+ "id": "123",
+ "product_id": "prod_123",
+ "post_id": "123",
+ "product": {
+ "id": "prod_123",
+ // other product fields...
+ },
+ "post": {
+ "id": "123",
+ // other post fields...
+ }
+}]
+```
+
+***
+
+## Apply Filters
+
+```ts highlights={[["4"], ["5"], ["6"]]}
+const { data: posts } = await query.graph({
+ entity: "post",
+ fields: ["id", "title"],
+ filters: {
+ id: "post_123",
+ },
+})
+```
+
+The `query.graph` function accepts a `filters` property. You can use this property to filter retrieved records.
+
+In the example above, you filter the `post` records by the ID `post_123`.
+
+You can also filter by multiple values of a property. For example:
+
+```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"], ["9"]]}
+const { data: posts } = await query.graph({
+ entity: "post",
+ fields: ["id", "title"],
+ filters: {
+ id: [
+ "post_123",
+ "post_321",
+ ],
+ },
+})
+```
+
+In the example above, you filter the `post` records by multiple IDs.
+
+Filters don't apply on fields of linked data models from other modules. Refer to the [Retrieve Linked Records](#retrieve-linked-records) section for an alternative solution.
+
+### Advanced Query Filters
+
+Under the hood, Query uses one of the following methods from the data model's module's service to retrieve records:
+
+- `listX` if you don't pass [pagination parameters](#apply-pagination). For example, `listPosts`.
+- `listAndCountX` if you pass pagination parameters. For example, `listAndCountPosts`.
+
+Both methods accepts a filter object that can be used to filter records.
+
+Those filters don't just allow you to filter by exact values. You can also filter by properties that don't match a value, match multiple values, and other filter types.
+
+Refer to the [Service Factory Reference](https://docs.medusajs.com/resources/service-factory-reference/tips/filtering/index.html.md) for examples of advanced filters. The following sections provide some quick examples.
+
+#### Filter by Not Matching a Value
+
+```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"]]}
+const { data: posts } = await query.graph({
+ entity: "post",
+ fields: ["id", "title"],
+ filters: {
+ title: {
+ $ne: null,
+ },
+ },
+})
+```
+
+In the example above, only posts that have a title are retrieved.
+
+#### Filter by Not Matching Multiple Values
+
+```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"]]}
+const { data: posts } = await query.graph({
+ entity: "post",
+ fields: ["id", "title"],
+ filters: {
+ title: {
+ $nin: ["My Post", "Another Post"],
+ },
+ },
+})
+```
+
+In the example above, only posts that don't have the title `My Post` or `Another Post` are retrieved.
+
+#### Filter by a Range
+
+```ts highlights={[["10"], ["11"], ["12"], ["13"], ["14"], ["15"]]}
+const startToday = new Date()
+startToday.setHours(0, 0, 0, 0)
+
+const endToday = new Date()
+endToday.setHours(23, 59, 59, 59)
+
+const { data: posts } = await query.graph({
+ entity: "post",
+ fields: ["id", "title"],
+ filters: {
+ published_at: {
+ $gt: startToday,
+ $lt: endToday,
+ },
+ },
+})
+```
+
+In the example above, only posts that were published today are retrieved.
+
+#### Filter Text by Like Value
+
+This filter only applies to text-like properties, including `text`, `id`, and `enum` properties.
+
+```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"]]}
+const { data: posts } = await query.graph({
+ entity: "post",
+ fields: ["id", "title"],
+ filters: {
+ title: {
+ $like: "%My%",
+ },
+ },
+})
+```
+
+In the example above, only posts that have the word `My` in their title are retrieved.
+
+#### Filter a Relation's Property
+
+```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"]]}
+const { data: posts } = await query.graph({
+ entity: "post",
+ fields: ["id", "title"],
+ filters: {
+ author: {
+ name: "John",
+ },
+ },
+})
+```
+
+While it's not possible to filter by a linked data model's property, you can filter by a relation's property (that is, the property of a related data model that is defined in the same module).
+
+In the example above, only posts that have an author with the name `John` are retrieved.
+
+***
+
+## Apply Pagination
+
+```ts highlights={[["8", "skip", "The number of records to skip before fetching the results."], ["9", "take", "The number of records to fetch."]]}
+const {
+ data: posts,
+ metadata: { count, take, skip } = {},
+} = await query.graph({
+ entity: "post",
+ fields: ["id", "title"],
+ pagination: {
+ skip: 0,
+ take: 10,
+ },
+})
+```
+
+The `graph` method's object parameter accepts a `pagination` property to configure the pagination of retrieved records.
+
+To paginate the returned records, pass the following properties to `pagination`:
+
+- `skip`: (required to apply pagination) The number of records to skip before fetching the results.
+- `take`: The number of records to fetch.
+
+When you provide the pagination fields, the `query.graph` method's returned object has a `metadata` property. Its value is an object having the following properties:
+
+- skip: (\`number\`) The number of records skipped.
+- take: (\`number\`) The number of records requested to fetch.
+- count: (\`number\`) The total number of records.
+
+### Sort Records
+
+```ts highlights={[["5"], ["6"], ["7"]]}
+const { data: posts } = await query.graph({
+ entity: "post",
+ fields: ["id", "title"],
+ pagination: {
+ order: {
+ name: "DESC",
+ },
+ },
+})
+```
+
+Sorting doesn't work on fields of linked data models from other modules.
+
+To sort returned records, pass an `order` property to `pagination`.
+
+The `order` property is an object whose keys are property names, and values are either:
+
+- `ASC` to sort records by that property in ascending order.
+- `DESC` to sort records by that property in descending order.
+
+***
+
+## Configure Query to Throw Errors
+
+By default, if Query doesn't find records matching your query, it returns an empty array. You can add option to configure Query to throw an error when no records are found.
+
+The `query.graph` method accepts as a second parameter an object that can have a `throwIfKeyNotFound` property. Its value is a boolean indicating whether to throw an error if no record is found when filtering by IDs. By default, it's `false`.
+
+For example:
+
+```ts
+const { data: posts } = await query.graph({
+ entity: "post",
+ fields: ["id", "title"],
+ filters: {
+ id: "post_123",
+ },
+}, {
+ throwIfKeyNotFound: true,
+})
+```
+
+In the example above, if no post is found with the ID `post_123`, Query will throw an error. This is useful to stop execution when a record is expected to exist.
+
+### Throw Error on Related Data Model
+
+The `throwIfKeyNotFound` option can also be used to throw an error if the ID of a related data model's record (in the same module) is passed in the filters, and the related record doesn't exist.
+
+For example:
+
+```ts
+const { data: posts } = await query.graph({
+ entity: "post",
+ fields: ["id", "title", "author.*"],
+ filters: {
+ id: "post_123",
+ author_id: "author_123",
+ },
+}, {
+ throwIfKeyNotFound: true,
+})
+```
+
+In the example above, Query throws an error either if no post is found with the ID `post_123` or if its found but its author ID isn't `author_123`.
+
+In the above example, it's assumed that a post belongs to an author, so it has an `author_id` property. However, this also works in the opposite case, where an author has many posts.
+
+For example:
+
+```ts
+const { data: posts } = await query.graph({
+ entity: "author",
+ fields: ["id", "name", "posts.*"],
+ filters: {
+ id: "author_123",
+ posts: {
+ id: "post_123",
+ },
+ },
+}, {
+ throwIfKeyNotFound: true,
+})
+```
+
+In the example above, Query throws an error if no author is found with the ID `author_123` or if the author is found but doesn't have a post with the ID `post_123`.
+
+***
+
+## Request Query Configurations
+
+For API routes that retrieve a single or list of resources, Medusa provides a `validateAndTransformQuery` middleware that:
+
+- Validates accepted query parameters, as explained in [this documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation/index.html.md).
+- Parses configurations that are received as query parameters to be passed to Query.
+
+Using this middleware allows you to have default configurations for retrieved fields and relations or pagination, while allowing clients to customize them per request.
+
+### Step 1: Add Middleware
+
+The first step is to use the `validateAndTransformQuery` middleware on the `GET` route. You add the middleware in `src/api/middlewares.ts`:
+
+```ts title="src/api/middlewares.ts"
+import {
+ validateAndTransformQuery,
+ defineMiddlewares,
+} from "@medusajs/framework/http"
+import { createFindParams } from "@medusajs/medusa/api/utils/validators"
+
+export const GetCustomSchema = createFindParams()
+
+export default defineMiddlewares({
+ routes: [
+ {
+ matcher: "/customs",
+ method: "GET",
+ middlewares: [
+ validateAndTransformQuery(
+ GetCustomSchema,
+ {
+ defaults: [
+ "id",
+ "title",
+ "products.*",
+ ],
+ isList: true,
+ }
+ ),
+ ],
+ },
+ ],
+})
+```
+
+The `validateAndTransformQuery` accepts two parameters:
+
+1. A Zod validation schema for the query parameters, which you can learn more about in the [API Route Validation documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation/index.html.md). Medusa has a `createFindParams` utility that generates a Zod schema that accepts four query parameters:
+ 1. `fields`: The fields and relations to retrieve in the returned resources.
+ 2. `offset`: The number of items to skip before retrieving the returned items.
+ 3. `limit`: The maximum number of items to return.
+ 4. `order`: The fields to order the returned items by in ascending or descending order.
+2. A Query configuration object. It accepts the following properties:
+ 1. `defaults`: An array of default fields and relations to retrieve in each resource.
+ 2. `isList`: A boolean indicating whether a list of items are returned in the response.
+ 3. `allowed`: An array of fields and relations allowed to be passed in the `fields` query parameter.
+ 4. `defaultLimit`: A number indicating the default limit to use if no limit is provided. By default, it's `50`.
+
+### Step 2: Use Configurations in API Route
+
+After applying this middleware, your API route now accepts the `fields`, `offset`, `limit`, and `order` query parameters mentioned above.
+
+The middleware transforms these parameters to configurations that you can pass to Query in your API route handler. These configurations are stored in the `queryConfig` parameter of the `MedusaRequest` object.
+
+As of [Medusa v2.2.0](https://github.com/medusajs/medusa/releases/tag/v2.2.0), `remoteQueryConfig` has been deprecated in favor of `queryConfig`. Their usage is still the same, only the property name has changed.
+
+For example, Create the file `src/api/customs/route.ts` with the following content:
+
+```ts title="src/api/customs/route.ts"
+import {
+ MedusaRequest,
+ MedusaResponse,
+} from "@medusajs/framework/http"
+import {
+ ContainerRegistrationKeys,
+} from "@medusajs/framework/utils"
+
+export const GET = async (
+ req: MedusaRequest,
+ res: MedusaResponse
+) => {
+ const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
+
+ const { data: posts } = await query.graph({
+ entity: "post",
+ ...req.queryConfig,
+ })
+
+ res.json({ posts: posts })
+}
+```
+
+This adds a `GET` API route at `/customs`, which is the API route you added the middleware for.
+
+In the API route, you pass `req.queryConfig` to `query.graph`. `queryConfig` has properties like `fields` and `pagination` to configure the query based on the default values you specified in the middleware, and the query parameters passed in the request.
+
+### Test it Out
+
+To test it out, start your Medusa application and send a `GET` request to the `/customs` API route. A list of records are retrieved with the specified fields in the middleware.
+
+```json title="Returned Data"
+{
+ "posts": [
+ {
+ "id": "123",
+ "title": "test"
+ }
+ ]
+}
+```
+
+Try passing one of the Query configuration parameters, like `fields` or `limit`, and you'll see its impact on the returned result.
+
+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.
+
+
# Commerce Modules
In this chapter, you'll learn about Medusa's Commerce Modules.
@@ -13152,13 +13152,45 @@ export const countProductsStep = createStep(
Your workflow can use services of both custom and Commerce Modules, supporting you in building custom flows without having to re-build core commerce features.
+# Infrastructure Modules
+
+In this chapter, you’ll learn about Infrastructure Modules.
+
+## What is an Infrastructure Module?
+
+An Infrastructure Module implements features and mechanisms related to the Medusa application’s architecture and infrastructure.
+
+Since modules are interchangeable, you have more control over Medusa’s architecture. For example, you can choose to use Memcached for event handling instead of Redis.
+
+***
+
+## Infrastructure Module Types
+
+There are different Infrastructure Module types including:
+
+
+
+- Cache Module: Defines the caching mechanism or logic to cache computational results.
+- Event Module: Integrates a pub/sub service to handle subscribing to and emitting events.
+- Workflow Engine Module: Integrates a service to store and track workflow executions and steps.
+- File Module: Integrates a storage service to handle uploading and managing files.
+- Notification Module: Integrates a third-party service or defines custom logic to send notifications to users and customers.
+- Locking Module: Integrates a service that manages access to shared resources by multiple processes or threads.
+
+***
+
+## Infrastructure Modules List
+
+Refer to the [Infrastructure Modules reference](https://docs.medusajs.com/resources/infrastructure-modules/index.html.md) for a list of Medusa’s Infrastructure Modules, available modules to install, and how to create an Infrastructure Module.
+
+
# Module Container
In this chapter, you'll learn about the module's container and how to resolve resources in that container.
-Since modules are isolated, each module has a local container only used by the resources of that module.
+Since modules are [isolated](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md), each module has a local container only used by the resources of that module.
-So, resources in the module, such as services or loaders, can only resolve other resources registered in the module's container.
+So, resources in the module, such as services or loaders, can only resolve other resources registered in the module's container, and some Framework tools that the Medusa application registers in the module's container.
### List of Registered Resources
@@ -13218,38 +13250,6 @@ export default async function helloWorldLoader({
```
-# Infrastructure Modules
-
-In this chapter, you’ll learn about Infrastructure Modules.
-
-## What is an Infrastructure Module?
-
-An Infrastructure Module implements features and mechanisms related to the Medusa application’s architecture and infrastructure.
-
-Since modules are interchangeable, you have more control over Medusa’s architecture. For example, you can choose to use Memcached for event handling instead of Redis.
-
-***
-
-## Infrastructure Module Types
-
-There are different Infrastructure Module types including:
-
-
-
-- Cache Module: Defines the caching mechanism or logic to cache computational results.
-- Event Module: Integrates a pub/sub service to handle subscribing to and emitting events.
-- Workflow Engine Module: Integrates a service to store and track workflow executions and steps.
-- File Module: Integrates a storage service to handle uploading and managing files.
-- Notification Module: Integrates a third-party service or defines custom logic to send notifications to users and customers.
-- Locking Module: Integrates a service that manages access to shared resources by multiple processes or threads.
-
-***
-
-## Infrastructure Modules List
-
-Refer to the [Infrastructure Modules reference](https://docs.medusajs.com/resources/infrastructure-modules/index.html.md) for a list of Medusa’s Infrastructure Modules, available modules to install, and how to create an Infrastructure Module.
-
-
# Perform Database Operations in a Service
In this chapter, you'll learn how to perform database operations in a module's service.
@@ -13862,7 +13862,8 @@ class BlogModuleService {
In this chapter, you'll learn how modules are isolated, and what that means for your custom development.
- Modules can't access resources, such as services or data models, from other modules.
-- Use Medusa's linking concepts, as explained in the [Module Links chapters](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md), to extend a module's data models and retrieve data across modules.
+- Use [Module Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) to extend an existing module's data models, and [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md) to retrieve data across modules.
+- Use [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) to build features that depend on functionalities from different modules.
## How are Modules Isolated?
@@ -13870,6 +13871,10 @@ A module is unaware of any resources other than its own, such as services or dat
For example, your custom module can't resolve the Product Module's main service or have direct relationships from its data model to the Product Module's data models.
+A module has its own container, as explained in the [Module Container](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md) chapter. This container includes the module's resources, such as services and data models, and some Framework resources that the Medusa application provides.
+
+Refer to the [Module Container Resources](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md) for a list of resources registered in a module's container.
+
***
## Why are Modules Isolated
@@ -13877,20 +13882,24 @@ For example, your custom module can't resolve the Product Module's main service
Some of the module isolation's benefits include:
- Integrate your module into any Medusa application without side-effects to your setup.
-- Replace existing modules with your custom implementation, if your use case is drastically different.
+- Replace existing modules with your custom implementation if your use case is drastically different.
- Use modules in other environments, such as Edge functions and Next.js apps.
***
## How to Extend Data Model of Another Module?
-To extend the data model of another module, such as the `product` data model of the Product Module, use Medusa's linking concepts as explained in the [Module Links chapters](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md).
+To extend the data model of another module, such as the `Product` data model of the Product Module, use [Module Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md). Module Links allow you to build associations between data models of different modules without breaking the module isolation.
+
+Then, you can retrieve data across modules using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md).
***
## How to Use Services of Other Modules?
-If you're building a feature that uses functionalities from different modules, use a workflow whose steps resolve the modules' services to perform these functionalities.
+You'll often build feature that uses functionalities from different modules. For example, if you may need to retrieve brands, then sync them to a third-party service.
+
+To build functionalities spanning across modules and systems, create a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) whose steps resolve the modules' services to perform these functionalities.
Workflows ensure data consistency through their roll-back mechanism and tracking of each execution's status, steps, input, and output.
@@ -13908,7 +13917,7 @@ const retrieveBrandsStep = createStep(
"retrieve-brands",
async (_, { container }) => {
const brandModuleService = container.resolve(
- "brandModuleService"
+ "brand"
)
const brands = await brandModuleService.listBrands()
@@ -13921,7 +13930,7 @@ const createBrandsInCmsStep = createStep(
"create-brands-in-cms",
async ({ brands }, { container }) => {
const cmsModuleService = container.resolve(
- "cmsModuleService"
+ "cms"
)
const cmsBrands = await cmsModuleService.createBrands(brands)
@@ -13930,7 +13939,7 @@ const createBrandsInCmsStep = createStep(
},
async (brands, { container }) => {
const cmsModuleService = container.resolve(
- "cmsModuleService"
+ "cms"
)
await cmsModuleService.deleteBrands(
@@ -13940,7 +13949,7 @@ const createBrandsInCmsStep = createStep(
)
```
-The `retrieveBrandsStep` retrieves the brands from a brand module, and the `createBrandsInCmsStep` creates the brands in a third-party system using a CMS module.
+The `retrieveBrandsStep` retrieves the brands from a Brand Module, and the `createBrandsInCmsStep` creates the brands in a third-party system using a CMS Module.
Then, create the following workflow that uses these steps:
@@ -13957,6 +13966,304 @@ export const syncBrandsWorkflow = createWorkflow(
You can then use this workflow in an API route, scheduled job, or other resources that use this functionality.
+***
+
+## How to Use Framework APIs and Tools in Module?
+
+### Framework Tools in Module Container
+
+A module has in its container some Framework APIs and tools, such as [Logger](https://docs.medusajs.com/learn/debugging-and-testing/logging/index.html.md). You can refer to the [Module Container Resources](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md) for a list of resources registered in a module's container.
+
+You can resolve those resources in the module's services and loaders.
+
+For example:
+
+```ts title="Example Service"
+import { Logger } from "@medusajs/framework/types"
+
+type InjectedDependencies = {
+ logger: Logger
+}
+
+export default class BlogModuleService {
+ protected logger_: Logger
+
+ constructor({ logger }: InjectedDependencies) {
+ this.logger_ = logger
+
+ this.logger_.info("[BlogModuleService]: Hello World!")
+ }
+
+ // ...
+}
+```
+
+In this example, the `BlogModuleService` class resolves the `Logger` service from the module's container and uses it to log a message.
+
+### Using Framework Tools in Workflows
+
+Some Framework APIs and tools are not registered in the module's container. For example, [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md) is only registered in the Medusa container.
+
+You should, instead, build workflows that use these APIs and tools along with your module's service.
+
+For example, you can create a workflow that retrieves data using Query, then pass the data to your module's service to perform some action.
+
+```ts title="Example Workflow"
+import { createWorkflow, createStep } from "@medusajs/framework/workflows-sdk"
+import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
+
+const createBrandsInCmsStep = createStep(
+ "create-brands-in-cms",
+ async ({ brands }, { container }) => {
+ const cmsModuleService = container.resolve(
+ "cms"
+ )
+
+ const cmsBrands = await cmsModuleService.createBrands(brands)
+
+ return new StepResponse(cmsBrands, cmsBrands)
+ },
+ async (brands, { container }) => {
+ const cmsModuleService = container.resolve(
+ "cms"
+ )
+
+ await cmsModuleService.deleteBrands(
+ brands.map((brand) => brand.id)
+ )
+ }
+)
+
+const syncBrandsWorkflow = createWorkflow(
+ "sync-brands",
+ () => {
+ const { data: brands } = useQueryGraphStep({
+ entity: "brand",
+ fields: [
+ "*",
+ "products.*",
+ ],
+ })
+
+ createBrandsInCmsStep({ brands })
+ }
+)
+```
+
+In this example, you use the `useQueryGraphStep` to retrieve brands with their products, then pass the brands to the `createBrandsInCmsStep` step.
+
+In the `createBrandsInCmsStep`, you resolve the CMS Module's service from the module's container and use it to create the brands in the third-party system. You pass the brands you retrieved using Query to the module's service.
+
+### Injecting Dependencies to Module
+
+Some cases still require you to access external resources, mainly [Infrastructure Modules](https://docs.medusajs.com/resources/infrastructure-modules/index.html.md) or Framework tools, in your module.
+For example, you may need the [Event Module](https://docs.medusajs.com/resources/infrastructure-modules/event/index.html.md) to emit events from your module's service.
+
+In those cases, you can inject the dependencies to your module's service in `medusa-config.ts` using the `dependencies` property of the module's configuration.
+
+Use this approach only when absolutely necessary, where workflows aren't sufficient for your use case. By injecting dependencies, you risk breaking your module if the dependency isn't provided, or if the dependency's API changes.
+
+For example:
+
+```ts title="medusa-config.ts"
+import { Modules } from "@medusajs/framework/utils"
+
+module.exports = defineConfig({
+ // ...
+ modules: [
+ {
+ resolve: "./src/modules/blog",
+ dependencies: [
+ Modules.EVENT_BUS,
+ ],
+ },
+ ],
+})
+```
+
+In this example, you inject the Event Module's service to your module's container.
+
+Only the main service will be injected into the module's container.
+
+You can then use the Event Module's service in your module's service:
+
+```ts title="Example Service"
+class BlogModuleService {
+ protected eventBusService_: AbstractEventBusModuleService
+
+ constructor({ event_bus }) {
+ this.eventBusService_ = event_bus
+ }
+
+ performAction() {
+ // TODO perform action
+
+ this.eventBusService_.emit({
+ name: "custom.event",
+ data: {
+ id: "123",
+ // other data payload
+ },
+ })
+ }
+}
+```
+
+
+# Multiple Services in a Module
+
+In this chapter, you'll learn how to use multiple services in a module.
+
+## Module's Main and Internal Services
+
+A module has one main service only, which is the service exported in the module's definition.
+
+However, you may use other services in your module to better organize your code or split functionalities. These are called internal services that can be resolved within your module, but not in external resources.
+
+***
+
+## How to Add an Internal Service
+
+### 1. Create Service
+
+To add an internal service, create it in the `services` directory of your module.
+
+For example, create the file `src/modules/blog/services/client.ts` with the following content:
+
+```ts title="src/modules/blog/services/client.ts"
+export class ClientService {
+ async getMessage(): Promise {
+ return "Hello, World!"
+ }
+}
+```
+
+### 2. Export Service in Index
+
+Next, create an `index.ts` file under the `services` directory of the module that exports your internal services.
+
+For example, create the file `src/modules/blog/services/index.ts` with the following content:
+
+```ts title="src/modules/blog/services/index.ts"
+export * from "./client"
+```
+
+This exports the `ClientService`.
+
+### 3. Resolve Internal Service
+
+Internal services exported in the `services/index.ts` file of your module are now registered in the container and can be resolved in other services in the module as well as loaders.
+
+For example, in your main service:
+
+```ts title="src/modules/blog/service.ts" highlights={[["5"], ["13"]]}
+// other imports...
+import { ClientService } from "./services"
+
+type InjectedDependencies = {
+ clientService: ClientService
+}
+
+class BlogModuleService extends MedusaService({
+ Post,
+}){
+ protected clientService_: ClientService
+
+ constructor({ clientService }: InjectedDependencies) {
+ super(...arguments)
+ this.clientService_ = clientService
+ }
+}
+```
+
+You can now use your internal service in your main service.
+
+***
+
+## Resolve Resources in Internal Service
+
+Resolve dependencies from your module's container in the constructor of your internal service.
+
+For example:
+
+```ts
+import { Logger } from "@medusajs/framework/types"
+
+type InjectedDependencies = {
+ logger: Logger
+}
+
+export class ClientService {
+ protected logger_: Logger
+
+ constructor({ logger }: InjectedDependencies) {
+ this.logger_ = logger
+ }
+}
+```
+
+***
+
+## Access Module Options
+
+Your internal service can't access the module's options.
+
+To retrieve the module's options, use the `configModule` registered in the module's container, which is the configurations in `medusa-config.ts`.
+
+For example:
+
+```ts
+import { ConfigModule } from "@medusajs/framework/types"
+import { BLOG_MODULE } from ".."
+
+export type InjectedDependencies = {
+ configModule: ConfigModule
+}
+
+export class ClientService {
+ protected options: Record
+
+ constructor({ configModule }: InjectedDependencies) {
+ const moduleDef = configModule.modules[BLOG_MODULE]
+
+ if (typeof moduleDef !== "boolean") {
+ this.options = moduleDef.options
+ }
+ }
+}
+```
+
+The `configModule` has a `modules` property that includes all registered modules. Retrieve the module's configuration using its registration key.
+
+If its value is not a `boolean`, set the service's options to the module configuration's `options` property.
+
+
+# Modules Directory Structure
+
+In this document, you'll learn about the expected files and directories in your module.
+
+
+
+## index.ts
+
+The `index.ts` file in the root of your module's directory is the only required file. It must export the module's definition as explained in a [previous chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md).
+
+***
+
+## service.ts
+
+A module must have a main service. It's created in the `service.ts` file at the root of your module directory as explained in a [previous chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md).
+
+***
+
+## Other Directories
+
+The following directories are optional and their content are explained more in the following chapters:
+
+- `models`: Holds the data models representing tables in the database.
+- `migrations`: Holds the migration files used to reflect changes on the database.
+- `loaders`: Holds the scripts to run on the Medusa application's start-up.
+
# Loaders
@@ -14044,12 +14351,18 @@ This indicates that the loader in the `hello` module ran and logged this message
## When are Loaders Executed?
+### Loaders Executed on Application Startup
+
When you start the Medusa application, it executes the loaders of all modules in their registration order.
A loader is executed before the module's main service is instantiated. So, you can use loaders to register in the module's container resources that you want to use in the module's service. For example, you can register a database connection.
Loaders are also useful to only load a module if a certain condition is met. For example, if you try to connect to a database in a loader but the connection fails, you can throw an error in the loader to prevent the module from being loaded. This is useful if your module depends on an external service to work.
+### Loaders Executed with Migrations
+
+Loaders are also executed when you run [migrations](https://docs.medusajs.com/learn/fundamentals/data-models/write-migration/index.html.md). This can be useful if you need to run some task before the migrations, or you want to migrate some data to an integrated third-party system as part of the migration process.
+
***
## Example: Register Custom MongoDB Connection
@@ -14211,161 +14524,6 @@ info: Connected to MongoDB
You can now resolve the MongoDB Module's main service in your customizations to perform operations on the MongoDB database.
-# Multiple Services in a Module
-
-In this chapter, you'll learn how to use multiple services in a module.
-
-## Module's Main and Internal Services
-
-A module has one main service only, which is the service exported in the module's definition.
-
-However, you may use other services in your module to better organize your code or split functionalities. These are called internal services that can be resolved within your module, but not in external resources.
-
-***
-
-## How to Add an Internal Service
-
-### 1. Create Service
-
-To add an internal service, create it in the `services` directory of your module.
-
-For example, create the file `src/modules/blog/services/client.ts` with the following content:
-
-```ts title="src/modules/blog/services/client.ts"
-export class ClientService {
- async getMessage(): Promise {
- return "Hello, World!"
- }
-}
-```
-
-### 2. Export Service in Index
-
-Next, create an `index.ts` file under the `services` directory of the module that exports your internal services.
-
-For example, create the file `src/modules/blog/services/index.ts` with the following content:
-
-```ts title="src/modules/blog/services/index.ts"
-export * from "./client"
-```
-
-This exports the `ClientService`.
-
-### 3. Resolve Internal Service
-
-Internal services exported in the `services/index.ts` file of your module are now registered in the container and can be resolved in other services in the module as well as loaders.
-
-For example, in your main service:
-
-```ts title="src/modules/blog/service.ts" highlights={[["5"], ["13"]]}
-// other imports...
-import { ClientService } from "./services"
-
-type InjectedDependencies = {
- clientService: ClientService
-}
-
-class BlogModuleService extends MedusaService({
- Post,
-}){
- protected clientService_: ClientService
-
- constructor({ clientService }: InjectedDependencies) {
- super(...arguments)
- this.clientService_ = clientService
- }
-}
-```
-
-You can now use your internal service in your main service.
-
-***
-
-## Resolve Resources in Internal Service
-
-Resolve dependencies from your module's container in the constructor of your internal service.
-
-For example:
-
-```ts
-import { Logger } from "@medusajs/framework/types"
-
-type InjectedDependencies = {
- logger: Logger
-}
-
-export class ClientService {
- protected logger_: Logger
-
- constructor({ logger }: InjectedDependencies) {
- this.logger_ = logger
- }
-}
-```
-
-***
-
-## Access Module Options
-
-Your internal service can't access the module's options.
-
-To retrieve the module's options, use the `configModule` registered in the module's container, which is the configurations in `medusa-config.ts`.
-
-For example:
-
-```ts
-import { ConfigModule } from "@medusajs/framework/types"
-import { BLOG_MODULE } from ".."
-
-export type InjectedDependencies = {
- configModule: ConfigModule
-}
-
-export class ClientService {
- protected options: Record
-
- constructor({ configModule }: InjectedDependencies) {
- const moduleDef = configModule.modules[BLOG_MODULE]
-
- if (typeof moduleDef !== "boolean") {
- this.options = moduleDef.options
- }
- }
-}
-```
-
-The `configModule` has a `modules` property that includes all registered modules. Retrieve the module's configuration using its registration key.
-
-If its value is not a `boolean`, set the service's options to the module configuration's `options` property.
-
-
-# Modules Directory Structure
-
-In this document, you'll learn about the expected files and directories in your module.
-
-
-
-## index.ts
-
-The `index.ts` file in the root of your module's directory is the only required file. It must export the module's definition as explained in a [previous chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md).
-
-***
-
-## service.ts
-
-A module must have a main service. It's created in the `service.ts` file at the root of your module directory as explained in a [previous chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md).
-
-***
-
-## Other Directories
-
-The following directories are optional and their content are explained more in the following chapters:
-
-- `models`: Holds the data models representing tables in the database.
-- `migrations`: Holds the migration files used to reflect changes on the database.
-- `loaders`: Holds the scripts to run on the Medusa application's start-up.
-
-
# Module 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.
@@ -14569,6 +14727,36 @@ 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.
+
+
# Service Factory
In this chapter, you’ll learn about what the service factory is and how to use it.
@@ -14744,105 +14932,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.
-
-
-# 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.
-
-
# Create a Plugin
In this chapter, you'll learn how to create a Medusa plugin and publish it.
@@ -15277,6 +15366,364 @@ npm publish
This will publish an updated version of your plugin under a new version.
+# 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.
+
+***
+
+## 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")
+```
+~~~ */}
+
+
# Compensation Function
In this chapter, you'll learn what a compensation function is and how to add it to a step.
@@ -15692,6 +16139,450 @@ Since `then` returns a value different than the step's result, you pass to the `
The second and third parameters are the same as the parameters you previously passed to `when`.
+# 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.
+
+
+# Execute Another Workflow
+
+In this chapter, you'll learn how to execute a workflow in another.
+
+## Execute in a Workflow
+
+To execute a workflow in another, use the `runAsStep` method that every workflow has.
+
+For example:
+
+```ts highlights={workflowsHighlights} collapsibleLines="1-7" expandMoreButton="Show Imports"
+import {
+ createWorkflow,
+} from "@medusajs/framework/workflows-sdk"
+import {
+ createProductsWorkflow,
+} from "@medusajs/medusa/core-flows"
+
+const workflow = createWorkflow(
+ "hello-world",
+ async (input) => {
+ const products = createProductsWorkflow.runAsStep({
+ input: {
+ products: [
+ // ...
+ ],
+ },
+ })
+
+ // ...
+ }
+)
+```
+
+Instead of invoking the workflow and passing it the container, you use its `runAsStep` method and pass it an object as a parameter.
+
+The object has an `input` property to pass input to the workflow.
+
+***
+
+## Preparing Input Data
+
+If you need to perform some data manipulation to prepare the other workflow's input data, use `transform` from the Workflows SDK.
+
+Learn about transform in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md).
+
+For example:
+
+```ts highlights={transformHighlights} collapsibleLines="1-12"
+import {
+ createWorkflow,
+ transform,
+} from "@medusajs/framework/workflows-sdk"
+import {
+ createProductsWorkflow,
+} from "@medusajs/medusa/core-flows"
+
+type WorkflowInput = {
+ title: string
+}
+
+const workflow = createWorkflow(
+ "hello-product",
+ async (input: WorkflowInput) => {
+ const createProductsData = transform({
+ input,
+ }, (data) => [
+ {
+ title: `Hello ${data.input.title}`,
+ },
+ ])
+
+ const products = createProductsWorkflow.runAsStep({
+ input: {
+ products: createProductsData,
+ },
+ })
+
+ // ...
+ }
+)
+```
+
+In this example, you use the `transform` function to prepend `Hello` to the title of the product. Then, you pass the result as an input to the `createProductsWorkflow`.
+
+***
+
+## Run Workflow Conditionally
+
+To run a workflow in another based on a condition, use when-then from the Workflows SDK.
+
+Learn about when-then in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/conditions/index.html.md).
+
+For example:
+
+```ts highlights={whenHighlights} collapsibleLines="1-16"
+import {
+ createWorkflow,
+ when,
+} from "@medusajs/framework/workflows-sdk"
+import {
+ createProductsWorkflow,
+} from "@medusajs/medusa/core-flows"
+import {
+ CreateProductWorkflowInputDTO,
+} from "@medusajs/framework/types"
+
+type WorkflowInput = {
+ product?: CreateProductWorkflowInputDTO
+ should_create?: boolean
+}
+
+const workflow = createWorkflow(
+ "hello-product",
+ async (input: WorkflowInput) => {
+ const product = when(input, ({ should_create }) => should_create)
+ .then(() => {
+ return createProductsWorkflow.runAsStep({
+ input: {
+ products: [input.product],
+ },
+ })
+ })
+ }
+)
+```
+
+In this example, you use when-then to run the `createProductsWorkflow` only if `should_create` (passed in the `input`) is enabled.
+
+
+# Error Handling in Workflows
+
+In this chapter, you’ll learn about what happens when an error occurs in a workflow, how to disable error throwing in a workflow, and try-catch alternatives in workflow definitions.
+
+## Default Behavior of Errors in Workflows
+
+When an error occurs in a workflow, such as when a step throws an error, the workflow execution stops. Then, [the compensation function](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md) of every step in the workflow is called to undo the actions performed by their respective steps.
+
+The workflow's caller, such as an API route, subscriber, or scheduled job, will also fail and stop execution. Medusa then logs the error in the console. For API routes, an appropriate error is returned to the client based on the thrown error.
+
+Learn more about error handling in API routes in the [Errors chapter](https://docs.medusajs.com/learn/fundamentals/api-routes/errors/index.html.md).
+
+This is the default behavior of errors in workflows. However, you can configure workflows to not throw errors, or you can configure a step's internal error handling mechanism to change the default behavior.
+
+***
+
+## Disable Error Throwing in Workflow
+
+When an error is thrown in the workflow, that means the caller of the workflow, such as an API route, will fail and stop execution as well.
+
+While this is the common behavior, there are certain cases where you want to handle the error differently. For example, you may want to check the errors thrown by the workflow and return a custom error response to the client.
+
+The object parameter of a workflow's `run` method accepts a `throwOnError` property. When this property is set to `false`, the workflow will stop execution if an error occurs, but the Medusa's workflow engine will catch that error and return it to the caller instead of throwing it.
+
+For example:
+
+```ts title="src/api/workflows/route.ts" highlights={highlights} collapsibleLines="1-6" expandButtonLabel="Show Imports"
+import type {
+ MedusaRequest,
+ MedusaResponse,
+} from "@medusajs/framework/http"
+import myWorkflow from "../../../workflows/hello-world"
+
+export async function GET(
+ req: MedusaRequest,
+ res: MedusaResponse
+) {
+ const { result, errors } = await myWorkflow(req.scope)
+ .run({
+ // ...
+ throwOnError: false,
+ })
+
+ if (errors.length) {
+ return res.send({
+ message: "Something unexpected happened. Please try again.",
+ })
+ }
+
+ res.send(result)
+}
+```
+
+You disable throwing errors in the workflow by setting the `throwOnError` property to `false` in the `run` method of the workflow.
+
+The object returned by the `run` method contains an `errors` property. This property is an array of errors that occured during the workflow's execution. You can check this array to see if any errors occurred and handle them accordingly.
+
+An error object has the following properties:
+
+- action: (\`string\`) The ID of the step that threw the error.
+- handlerType: (\`invoke\` \\| \`compensate\`) Where the error occurred. If the value is \`invoke\`, it means the error occurred in a step. Otherwise, the error occurred in the compensation function of a step.
+- error: (\[Error]\(https://nodejs.org/docs/latest-v20.x/api/errors.html#class-error)) The error object that was thrown.
+
+***
+
+## Try-Catch Alternatives in Workflow Definition
+
+If you want to use try-catch mechanism in a workflow to undo step actions, use a [compensation function](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md) instead.
+
+### Why You Can't Use Try-Catch in Workflow Definitions
+
+Medusa creates an internal representation of the workflow definition you pass to `createWorkflow` to track and store its steps.
+
+At that point, variables in the workflow don't have any values. They only do when you execute the workflow.
+
+So, try-catch blocks in the workflow definition function won't have an effect, as at that time the workflow is not executed and errors are not thrown.
+
+You can still use try-catch blocks in a workflow's step functions. For cases that require granular control over error handling in a workflow's definition, you can configure the internal error handling mechanism of a step.
+
+### Skip Workflow on Step Failure
+
+A step has a `skipOnPermanentFailure` configuration that allows you to configure what happens when an error occurs in the step. Its value can be a boolean or a string.
+
+By default, `skipOnPermanentFailure` is disabled. When it's enabled, the workflow's status is set to `skipped` instead of `failed`. This means:
+
+- Compensation functions of the workflow's steps are not called.
+- The workflow's caller continues executing. You can still [access the error](#disable-error-throwing-in-workflow) that occurred during the workflow's execution as mentioned in the [Disable Error Throwing](#disable-error-throwing-in-workflow) section.
+
+This is useful when you want to perform actions if no error occurs, but you don't care about compensating the workflow's steps or you don't want to stop the caller's execution.
+
+You can think of setting the `skipOnPermanentFailure` to `true` as the equivalent of the following `try-catch` block:
+
+```ts title="Outside a Workflow"
+try {
+ actionThatThrowsError()
+
+ moreActions()
+} catch (e) {
+ // don't do anything
+}
+```
+
+You can do this in a workflow using the step's `skipOnPermanentFailure` configuration:
+
+```ts title="Workflow Equivalent" highlights={skipOnPermanentFailureEnabledHighlights}
+import {
+ createWorkflow,
+} from "@medusajs/framework/workflows-sdk"
+import {
+ actionThatThrowsError,
+ moreActions,
+} from "./steps"
+
+export const myWorkflow = createWorkflow(
+ "hello-world",
+ function (input) {
+ actionThatThrowsError().config({
+ skipOnPermanentFailure: true,
+ })
+
+ // This action will not be executed if the previous step throws an error
+ moreActions()
+ }
+)
+```
+
+You set the configuration of a step by chaining the `config` method to the step's function call. The `config` method accepts an object similar to the one that can be passed to `createStep`.
+
+In this example, if the `actionThatThrowsError` step throws an error, the rest of the workflow will be skipped, and the `moreActions` step will not be executed.
+
+You can then access the error that occurred in that step as explained in the [Disable Error Throwing](#disable-error-throwing-in-workflow) section.
+
+### Continue Workflow Execution from a Specific Step
+
+In some cases, if an error occurs in a step, you may want to continue the workflow's execution from a specific step instead of stopping the workflow's execution or skipping the rest of the steps.
+
+The `skipOnPermanentFailure` configuration can accept a step's ID as a value. Then, the workflow will continue execution from that step if an error occurs in the step that has the `skipOnPermanentFailure` configuration.
+
+The compensation function of the step that has the `skipOnPermanentFailure` configuration will not be called when an error occurs.
+
+You can think of setting the `skipOnPermanentFailure` to a step's ID as the equivalent of the following `try-catch` block:
+
+```ts title="Outside a Workflow"
+try {
+ actionThatThrowsError()
+
+ moreActions()
+} catch (e) {
+ // do nothing
+}
+
+continueExecutionFromStep()
+```
+
+You can do this in a workflow using the step's `skipOnPermanentFailure` configuration:
+
+```ts title="Workflow Equivalent" highlights={skipOnPermanentFailureStepHighlights}
+import {
+ createWorkflow,
+} from "@medusajs/framework/workflows-sdk"
+import {
+ actionThatThrowsError,
+ moreActions,
+ continueExecutionFromStep,
+} from "./steps"
+
+export const myWorkflow = createWorkflow(
+ "hello-world",
+ function (input) {
+ actionThatThrowsError().config({
+ // The `continue-execution-from-step` is the ID passed as a first
+ // parameter to `createStep` of `continueExecutionFromStep`.
+ skipOnPermanentFailure: "continue-execution-from-step",
+ })
+
+ // This action will not be executed if the previous step throws an error
+ moreActions()
+
+ // This action will be executed either way
+ continueExecutionFromStep()
+ }
+)
+```
+
+In this example, you configure the `actionThatThrowsError` step to continue the workflow's execution from the `continueExecutionFromStep` step if an error occurs in the `actionThatThrowsError` step.
+
+Notice that you pass the ID of the `continueExecutionFromStep` step as it's set in the `createStep` function.
+
+So, the `moreActions` step will not be executed if the `actionThatThrowsError` step throws an error, and the `continueExecutionFromStep` will be executed anyway.
+
+You can then access the error that occurred in that step as explained in the [Disable Error Throwing](#disable-error-throwing-in-workflow) section.
+
+If the specified step ID doesn't exist in the workflow, it will be equivalent to setting the `skipOnPermanentFailure` configuration to `true`. So, the workflow will be skipped, and the rest of the steps will not be executed.
+
+### Set Step as Failed, but Continue Workflow Execution
+
+In some cases, you may want to fail a step, but continue the rest of the workflow's execution.
+
+This is useful when you don't want a step's failure to stop the workflow's execution, but you want to mark that step as failed.
+
+The `continueOnPermanentFailure` configuration allows you to do that. When enabled, the workflow's execution will continue, but the step will be marked as failed if an error occurs in that step.
+
+The compensation function of the step that has the `continueOnPermanentFailure` configuration will not be called when an error occurs.
+
+You can think of setting the `continueOnPermanentFailure` to `true` as the equivalent of the following `try-catch` block:
+
+```ts title="Outside a Workflow"
+try {
+ actionThatThrowsError()
+} catch (e) {
+ // do nothing
+}
+
+moreActions()
+```
+
+You can do this in a workflow using the step's `continueOnPermanentFailure` configuration:
+
+```ts title="Workflow Equivalent" highlights={continueOnPermanentFailureHighlights}
+import {
+ createWorkflow,
+} from "@medusajs/framework/workflows-sdk"
+import {
+ actionThatThrowsError,
+ moreActions,
+} from "./steps"
+
+export const myWorkflow = createWorkflow(
+ "hello-world",
+ function (input) {
+ actionThatThrowsError().config({
+ continueOnPermanentFailure: true,
+ })
+
+ // This action will be executed even if the previous step throws an error
+ moreActions()
+ }
+)
+```
+
+In this example, if the `actionThatThrowsError` step throws an error, the `moreActions` step will still be executed.
+
+You can then access the error that occurred in that step as explained in the [Disable Error Throwing](#disable-error-throwing-in-workflow) section.
+
+
# Workflow Constraints
This chapter lists constraints of defining a workflow or its steps.
@@ -16046,381 +16937,6 @@ const step1 = createStep(
```
-# Error Handling in Workflows
-
-In this chapter, you’ll learn about what happens when an error occurs in a workflow, how to disable error throwing in a workflow, and try-catch alternatives in workflow definitions.
-
-## Default Behavior of Errors in Workflows
-
-When an error occurs in a workflow, such as when a step throws an error, the workflow execution stops. Then, [the compensation function](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md) of every step in the workflow is called to undo the actions performed by their respective steps.
-
-The workflow's caller, such as an API route, subscriber, or scheduled job, will also fail and stop execution. Medusa then logs the error in the console. For API routes, an appropriate error is returned to the client based on the thrown error.
-
-Learn more about error handling in API routes in the [Errors chapter](https://docs.medusajs.com/learn/fundamentals/api-routes/errors/index.html.md).
-
-This is the default behavior of errors in workflows. However, you can configure workflows to not throw errors, or you can configure a step's internal error handling mechanism to change the default behavior.
-
-***
-
-## Disable Error Throwing in Workflow
-
-When an error is thrown in the workflow, that means the caller of the workflow, such as an API route, will fail and stop execution as well.
-
-While this is the common behavior, there are certain cases where you want to handle the error differently. For example, you may want to check the errors thrown by the workflow and return a custom error response to the client.
-
-The object parameter of a workflow's `run` method accepts a `throwOnError` property. When this property is set to `false`, the workflow will stop execution if an error occurs, but the Medusa's workflow engine will catch that error and return it to the caller instead of throwing it.
-
-For example:
-
-```ts title="src/api/workflows/route.ts" highlights={highlights} collapsibleLines="1-6" expandButtonLabel="Show Imports"
-import type {
- MedusaRequest,
- MedusaResponse,
-} from "@medusajs/framework/http"
-import myWorkflow from "../../../workflows/hello-world"
-
-export async function GET(
- req: MedusaRequest,
- res: MedusaResponse
-) {
- const { result, errors } = await myWorkflow(req.scope)
- .run({
- // ...
- throwOnError: false,
- })
-
- if (errors.length) {
- return res.send({
- message: "Something unexpected happened. Please try again.",
- })
- }
-
- res.send(result)
-}
-```
-
-You disable throwing errors in the workflow by setting the `throwOnError` property to `false` in the `run` method of the workflow.
-
-The object returned by the `run` method contains an `errors` property. This property is an array of errors that occured during the workflow's execution. You can check this array to see if any errors occurred and handle them accordingly.
-
-An error object has the following properties:
-
-- action: (\`string\`) The ID of the step that threw the error.
-- handlerType: (\`invoke\` \\| \`compensate\`) Where the error occurred. If the value is \`invoke\`, it means the error occurred in a step. Otherwise, the error occurred in the compensation function of a step.
-- error: (\[Error]\(https://nodejs.org/docs/latest-v20.x/api/errors.html#class-error)) The error object that was thrown.
-
-***
-
-## Try-Catch Alternatives in Workflow Definition
-
-If you want to use try-catch mechanism in a workflow to undo step actions, use a [compensation function](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md) instead.
-
-### Why You Can't Use Try-Catch in Workflow Definitions
-
-Medusa creates an internal representation of the workflow definition you pass to `createWorkflow` to track and store its steps.
-
-At that point, variables in the workflow don't have any values. They only do when you execute the workflow.
-
-So, try-catch blocks in the workflow definition function won't have an effect, as at that time the workflow is not executed and errors are not thrown.
-
-You can still use try-catch blocks in a workflow's step functions. For cases that require granular control over error handling in a workflow's definition, you can configure the internal error handling mechanism of a step.
-
-### Skip Workflow on Step Failure
-
-A step has a `skipOnPermanentFailure` configuration that allows you to configure what happens when an error occurs in the step. Its value can be a boolean or a string.
-
-By default, `skipOnPermanentFailure` is disabled. When it's enabled, the workflow's status is set to `skipped` instead of `failed`. This means:
-
-- Compensation functions of the workflow's steps are not called.
-- The workflow's caller continues executing. You can still [access the error](#disable-error-throwing-in-workflow) that occurred during the workflow's execution as mentioned in the [Disable Error Throwing](#disable-error-throwing-in-workflow) section.
-
-This is useful when you want to perform actions if no error occurs, but you don't care about compensating the workflow's steps or you don't want to stop the caller's execution.
-
-You can think of setting the `skipOnPermanentFailure` to `true` as the equivalent of the following `try-catch` block:
-
-```ts title="Outside a Workflow"
-try {
- actionThatThrowsError()
-
- moreActions()
-} catch (e) {
- // don't do anything
-}
-```
-
-You can do this in a workflow using the step's `skipOnPermanentFailure` configuration:
-
-```ts title="Workflow Equivalent" highlights={skipOnPermanentFailureEnabledHighlights}
-import {
- createWorkflow,
-} from "@medusajs/framework/workflows-sdk"
-import {
- actionThatThrowsError,
- moreActions,
-} from "./steps"
-
-export const myWorkflow = createWorkflow(
- "hello-world",
- function (input) {
- actionThatThrowsError().config({
- skipOnPermanentFailure: true,
- })
-
- // This action will not be executed if the previous step throws an error
- moreActions()
- }
-)
-```
-
-You set the configuration of a step by chaining the `config` method to the step's function call. The `config` method accepts an object similar to the one that can be passed to `createStep`.
-
-In this example, if the `actionThatThrowsError` step throws an error, the rest of the workflow will be skipped, and the `moreActions` step will not be executed.
-
-You can then access the error that occurred in that step as explained in the [Disable Error Throwing](#disable-error-throwing-in-workflow) section.
-
-### Continue Workflow Execution from a Specific Step
-
-In some cases, if an error occurs in a step, you may want to continue the workflow's execution from a specific step instead of stopping the workflow's execution or skipping the rest of the steps.
-
-The `skipOnPermanentFailure` configuration can accept a step's ID as a value. Then, the workflow will continue execution from that step if an error occurs in the step that has the `skipOnPermanentFailure` configuration.
-
-The compensation function of the step that has the `skipOnPermanentFailure` configuration will not be called when an error occurs.
-
-You can think of setting the `skipOnPermanentFailure` to a step's ID as the equivalent of the following `try-catch` block:
-
-```ts title="Outside a Workflow"
-try {
- actionThatThrowsError()
-
- moreActions()
-} catch (e) {
- // do nothing
-}
-
-continueExecutionFromStep()
-```
-
-You can do this in a workflow using the step's `skipOnPermanentFailure` configuration:
-
-```ts title="Workflow Equivalent" highlights={skipOnPermanentFailureStepHighlights}
-import {
- createWorkflow,
-} from "@medusajs/framework/workflows-sdk"
-import {
- actionThatThrowsError,
- moreActions,
- continueExecutionFromStep,
-} from "./steps"
-
-export const myWorkflow = createWorkflow(
- "hello-world",
- function (input) {
- actionThatThrowsError().config({
- // The `continue-execution-from-step` is the ID passed as a first
- // parameter to `createStep` of `continueExecutionFromStep`.
- skipOnPermanentFailure: "continue-execution-from-step",
- })
-
- // This action will not be executed if the previous step throws an error
- moreActions()
-
- // This action will be executed either way
- continueExecutionFromStep()
- }
-)
-```
-
-In this example, you configure the `actionThatThrowsError` step to continue the workflow's execution from the `continueExecutionFromStep` step if an error occurs in the `actionThatThrowsError` step.
-
-Notice that you pass the ID of the `continueExecutionFromStep` step as it's set in the `createStep` function.
-
-So, the `moreActions` step will not be executed if the `actionThatThrowsError` step throws an error, and the `continueExecutionFromStep` will be executed anyway.
-
-You can then access the error that occurred in that step as explained in the [Disable Error Throwing](#disable-error-throwing-in-workflow) section.
-
-If the specified step ID doesn't exist in the workflow, it will be equivalent to setting the `skipOnPermanentFailure` configuration to `true`. So, the workflow will be skipped, and the rest of the steps will not be executed.
-
-### Set Step as Failed, but Continue Workflow Execution
-
-In some cases, you may want to fail a step, but continue the rest of the workflow's execution.
-
-This is useful when you don't want a step's failure to stop the workflow's execution, but you want to mark that step as failed.
-
-The `continueOnPermanentFailure` configuration allows you to do that. When enabled, the workflow's execution will continue, but the step will be marked as failed if an error occurs in that step.
-
-The compensation function of the step that has the `continueOnPermanentFailure` configuration will not be called when an error occurs.
-
-You can think of setting the `continueOnPermanentFailure` to `true` as the equivalent of the following `try-catch` block:
-
-```ts title="Outside a Workflow"
-try {
- actionThatThrowsError()
-} catch (e) {
- // do nothing
-}
-
-moreActions()
-```
-
-You can do this in a workflow using the step's `continueOnPermanentFailure` configuration:
-
-```ts title="Workflow Equivalent" highlights={continueOnPermanentFailureHighlights}
-import {
- createWorkflow,
-} from "@medusajs/framework/workflows-sdk"
-import {
- actionThatThrowsError,
- moreActions,
-} from "./steps"
-
-export const myWorkflow = createWorkflow(
- "hello-world",
- function (input) {
- actionThatThrowsError().config({
- continueOnPermanentFailure: true,
- })
-
- // This action will be executed even if the previous step throws an error
- moreActions()
- }
-)
-```
-
-In this example, if the `actionThatThrowsError` step throws an error, the `moreActions` step will still be executed.
-
-You can then access the error that occurred in that step as explained in the [Disable Error Throwing](#disable-error-throwing-in-workflow) section.
-
-
-# Execute Another Workflow
-
-In this chapter, you'll learn how to execute a workflow in another.
-
-## Execute in a Workflow
-
-To execute a workflow in another, use the `runAsStep` method that every workflow has.
-
-For example:
-
-```ts highlights={workflowsHighlights} collapsibleLines="1-7" expandMoreButton="Show Imports"
-import {
- createWorkflow,
-} from "@medusajs/framework/workflows-sdk"
-import {
- createProductsWorkflow,
-} from "@medusajs/medusa/core-flows"
-
-const workflow = createWorkflow(
- "hello-world",
- async (input) => {
- const products = createProductsWorkflow.runAsStep({
- input: {
- products: [
- // ...
- ],
- },
- })
-
- // ...
- }
-)
-```
-
-Instead of invoking the workflow and passing it the container, you use its `runAsStep` method and pass it an object as a parameter.
-
-The object has an `input` property to pass input to the workflow.
-
-***
-
-## Preparing Input Data
-
-If you need to perform some data manipulation to prepare the other workflow's input data, use `transform` from the Workflows SDK.
-
-Learn about transform in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md).
-
-For example:
-
-```ts highlights={transformHighlights} collapsibleLines="1-12"
-import {
- createWorkflow,
- transform,
-} from "@medusajs/framework/workflows-sdk"
-import {
- createProductsWorkflow,
-} from "@medusajs/medusa/core-flows"
-
-type WorkflowInput = {
- title: string
-}
-
-const workflow = createWorkflow(
- "hello-product",
- async (input: WorkflowInput) => {
- const createProductsData = transform({
- input,
- }, (data) => [
- {
- title: `Hello ${data.input.title}`,
- },
- ])
-
- const products = createProductsWorkflow.runAsStep({
- input: {
- products: createProductsData,
- },
- })
-
- // ...
- }
-)
-```
-
-In this example, you use the `transform` function to prepend `Hello` to the title of the product. Then, you pass the result as an input to the `createProductsWorkflow`.
-
-***
-
-## Run Workflow Conditionally
-
-To run a workflow in another based on a condition, use when-then from the Workflows SDK.
-
-Learn about when-then in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/conditions/index.html.md).
-
-For example:
-
-```ts highlights={whenHighlights} collapsibleLines="1-16"
-import {
- createWorkflow,
- when,
-} from "@medusajs/framework/workflows-sdk"
-import {
- createProductsWorkflow,
-} from "@medusajs/medusa/core-flows"
-import {
- CreateProductWorkflowInputDTO,
-} from "@medusajs/framework/types"
-
-type WorkflowInput = {
- product?: CreateProductWorkflowInputDTO
- should_create?: boolean
-}
-
-const workflow = createWorkflow(
- "hello-product",
- async (input: WorkflowInput) => {
- const product = when(input, ({ should_create }) => should_create)
- .then(() => {
- return createProductsWorkflow.runAsStep({
- input: {
- products: [input.product],
- },
- })
- })
- }
-)
-```
-
-In this example, you use when-then to run the `createProductsWorkflow` only if `should_create` (passed in the `input`) is enabled.
-
-
# Long-Running Workflows
In this chapter, you’ll learn what a long-running workflow is and how to configure it.
@@ -16843,129 +17359,6 @@ It returns an array of the steps' results in the same order they're passed to th
So, `prices` is the result of `createPricesStep`, and `productSalesChannel` is the result of `attachProductToSalesChannelStep`.
-# Retry Failed Steps
-
-In this chapter, you’ll learn how to configure steps to allow retrial on failure.
-
-## What is a Step Retrial?
-
-A step retrial is a mechanism that allows a step to be retried automatically when it fails. This is useful for handling transient errors, such as network issues or temporary unavailability of a service.
-
-When a step fails, the workflow engine can automatically retry the step a specified number of times before marking the workflow as failed. This can help improve the reliability and resilience of your workflows.
-
-You can also configure the interval between retries, allowing you to wait for a certain period before attempting the step again. This is useful when the failure is due to a temporary issue that may resolve itself after some time.
-
-For example, if a step captures a payment, you may want to retry it the next day until the payment is successful or the maximum number of retries is reached.
-
-***
-
-## Configure a Step’s Retrial
-
-By default, when an error occurs in a step, the step and the workflow fail, and the execution stops.
-
-You can configure the step to retry on failure. The `createStep` function can accept a configuration object instead of the step’s name as a first parameter.
-
-For example:
-
-```ts title="src/workflows/hello-world.ts" highlights={[["10"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
-import {
- createStep,
- createWorkflow,
- WorkflowResponse,
-} from "@medusajs/framework/workflows-sdk"
-
-const step1 = createStep(
- {
- name: "step-1",
- maxRetries: 2,
- },
- async () => {
- console.log("Executing step 1")
-
- throw new Error("Oops! Something happened.")
- }
-)
-
-const myWorkflow = createWorkflow(
- "hello-world",
- function () {
- const str1 = step1()
-
- return new WorkflowResponse({
- message: str1,
- })
-})
-
-export default myWorkflow
-```
-
-The step’s configuration object accepts a `maxRetries` property, which is a number indicating the number of times a step can be retried when it fails.
-
-When you execute the above workflow, you’ll see the following result in the terminal:
-
-```bash
-Executing step 1
-Executing step 1
-Executing step 1
-error: Oops! Something happened.
-Error: Oops! Something happened.
-```
-
-The first line indicates the first time the step was executed, and the next two lines indicate the times the step was retried. After that, the step and workflow fail.
-
-***
-
-## Step Retry Intervals
-
-By default, a step is retried immediately after it fails. To specify a wait time before a step is retried, pass a `retryInterval` property to the step's configuration object. Its value is a number of seconds to wait before retrying the step.
-
-For example:
-
-```ts title="src/workflows/hello-world.ts" highlights={[["5"]]}
-const step1 = createStep(
- {
- name: "step-1",
- maxRetries: 2,
- retryInterval: 2, // 2 seconds
- },
- async () => {
- // ...
- }
-)
-```
-
-In this example, if the step fails, it will be retried after two seconds.
-
-### Maximum Retry Interval
-
-The `retryInterval` property's maximum value is [Number.MAX\_SAFE\_INTEGER](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER). So, you can set a very long wait time before the step is retried, allowing you to retry steps after a long period.
-
-For example, to retry a step after a day:
-
-```ts title="src/workflows/hello-world.ts" highlights={[["5"]]}
-const step1 = createStep(
- {
- name: "step-1",
- maxRetries: 2,
- retryInterval: 86400, // 1 day
- },
- async () => {
- // ...
- }
-)
-```
-
-In this example, if the step fails, it will be retried after `86400` seconds (one day).
-
-### Interval Changes Workflow to Long-Running
-
-By setting `retryInterval` on a step, a workflow that uses that step becomes a [long-running workflow](https://docs.medusajs.com/learn/fundamentals/workflows/long-running-workflow/index.html.md) that runs asynchronously in the background. This is useful when creating workflows that may fail and should run for a long time until they succeed, such as waiting for a payment to be captured or a shipment to be delivered.
-
-However, since the long-running workflow runs in the background, you won't receive its result or errors immediately when you execute the workflow.
-
-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).
-
-
# Store Workflow Executions
In this chapter, you'll learn how to store workflow executions in the database and access them later.
@@ -17111,6 +17504,129 @@ if (workflowExecution.state === "failed") {
Other state values include `done`, `invoking`, and `compensating`.
+# Retry Failed Steps
+
+In this chapter, you’ll learn how to configure steps to allow retrial on failure.
+
+## What is a Step Retrial?
+
+A step retrial is a mechanism that allows a step to be retried automatically when it fails. This is useful for handling transient errors, such as network issues or temporary unavailability of a service.
+
+When a step fails, the workflow engine can automatically retry the step a specified number of times before marking the workflow as failed. This can help improve the reliability and resilience of your workflows.
+
+You can also configure the interval between retries, allowing you to wait for a certain period before attempting the step again. This is useful when the failure is due to a temporary issue that may resolve itself after some time.
+
+For example, if a step captures a payment, you may want to retry it the next day until the payment is successful or the maximum number of retries is reached.
+
+***
+
+## Configure a Step’s Retrial
+
+By default, when an error occurs in a step, the step and the workflow fail, and the execution stops.
+
+You can configure the step to retry on failure. The `createStep` function can accept a configuration object instead of the step’s name as a first parameter.
+
+For example:
+
+```ts title="src/workflows/hello-world.ts" highlights={[["10"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
+import {
+ createStep,
+ createWorkflow,
+ WorkflowResponse,
+} from "@medusajs/framework/workflows-sdk"
+
+const step1 = createStep(
+ {
+ name: "step-1",
+ maxRetries: 2,
+ },
+ async () => {
+ console.log("Executing step 1")
+
+ throw new Error("Oops! Something happened.")
+ }
+)
+
+const myWorkflow = createWorkflow(
+ "hello-world",
+ function () {
+ const str1 = step1()
+
+ return new WorkflowResponse({
+ message: str1,
+ })
+})
+
+export default myWorkflow
+```
+
+The step’s configuration object accepts a `maxRetries` property, which is a number indicating the number of times a step can be retried when it fails.
+
+When you execute the above workflow, you’ll see the following result in the terminal:
+
+```bash
+Executing step 1
+Executing step 1
+Executing step 1
+error: Oops! Something happened.
+Error: Oops! Something happened.
+```
+
+The first line indicates the first time the step was executed, and the next two lines indicate the times the step was retried. After that, the step and workflow fail.
+
+***
+
+## Step Retry Intervals
+
+By default, a step is retried immediately after it fails. To specify a wait time before a step is retried, pass a `retryInterval` property to the step's configuration object. Its value is a number of seconds to wait before retrying the step.
+
+For example:
+
+```ts title="src/workflows/hello-world.ts" highlights={[["5"]]}
+const step1 = createStep(
+ {
+ name: "step-1",
+ maxRetries: 2,
+ retryInterval: 2, // 2 seconds
+ },
+ async () => {
+ // ...
+ }
+)
+```
+
+In this example, if the step fails, it will be retried after two seconds.
+
+### Maximum Retry Interval
+
+The `retryInterval` property's maximum value is [Number.MAX\_SAFE\_INTEGER](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER). So, you can set a very long wait time before the step is retried, allowing you to retry steps after a long period.
+
+For example, to retry a step after a day:
+
+```ts title="src/workflows/hello-world.ts" highlights={[["5"]]}
+const step1 = createStep(
+ {
+ name: "step-1",
+ maxRetries: 2,
+ retryInterval: 86400, // 1 day
+ },
+ async () => {
+ // ...
+ }
+)
+```
+
+In this example, if the step fails, it will be retried after `86400` seconds (one day).
+
+### Interval Changes Workflow to Long-Running
+
+By setting `retryInterval` on a step, a workflow that uses that step becomes a [long-running workflow](https://docs.medusajs.com/learn/fundamentals/workflows/long-running-workflow/index.html.md) that runs asynchronously in the background. This is useful when creating workflows that may fail and should run for a long time until they succeed, such as waiting for a payment to be captured or a shipment to be delivered.
+
+However, since the long-running workflow runs in the background, you won't receive its result or errors immediately when you execute the workflow.
+
+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).
+
+
# Data Manipulation in Workflows with transform
In this chapter, you'll learn how to use `transform` from the Workflows SDK to manipulate data and variables in a workflow.
@@ -17316,6 +17832,162 @@ const myWorkflow = createWorkflow(
```
+# Workflow Timeout
+
+In this chapter, you’ll learn how to set a timeout for workflows and steps.
+
+## What is a Workflow Timeout?
+
+By default, a workflow doesn’t have a timeout. It continues execution until it’s finished or an error occurs.
+
+You can configure a workflow’s timeout to indicate how long the workflow can execute. If a workflow's execution time passes the configured timeout, it is failed and an error is thrown.
+
+### Timeout Doesn't Stop Step Execution
+
+Configuring a timeout doesn't stop the execution of a step in progress. The timeout only affects the status of the workflow and its result.
+
+***
+
+## Configure Workflow Timeout
+
+The `createWorkflow` function can accept a configuration object instead of the workflow’s name.
+
+In the configuration object, you pass a `timeout` property, whose value is a number indicating the timeout in seconds.
+
+For example:
+
+```ts title="src/workflows/hello-world.ts" highlights={[["16"]]} collapsibleLines="1-13" expandButtonLabel="Show More"
+import {
+ createStep,
+ createWorkflow,
+ WorkflowResponse,
+} from "@medusajs/framework/workflows-sdk"
+
+const step1 = createStep(
+ "step-1",
+ async () => {
+ // ...
+ }
+)
+
+const myWorkflow = createWorkflow({
+ name: "hello-world",
+ timeout: 2, // 2 seconds
+}, function () {
+ const str1 = step1()
+
+ return new WorkflowResponse({
+ message: str1,
+ })
+})
+
+export default myWorkflow
+
+```
+
+This workflow's executions fail if they run longer than two seconds.
+
+A workflow’s timeout error is returned in the `errors` property of the workflow’s execution, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/errors/index.html.md). The error’s name is `TransactionTimeoutError`.
+
+***
+
+## Configure Step Timeout
+
+Alternatively, you can configure the timeout for a step rather than the entire workflow.
+
+As mentioned in the previous section, the timeout doesn't stop the execution of the step. It only affects the step's status and output.
+
+The step’s configuration object accepts a `timeout` property, whose value is a number indicating the timeout in seconds.
+
+For example:
+
+```tsx
+const step1 = createStep(
+ {
+ name: "step-1",
+ timeout: 2, // 2 seconds
+ },
+ async () => {
+ // ...
+ }
+)
+```
+
+This step's executions fail if they run longer than two seconds.
+
+A step’s timeout error is returned in the `errors` property of the workflow’s execution, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/errors/index.html.md). The error’s name is `TransactionStepTimeoutError`.
+
+
+# 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.
+
+
# Workflow Hooks
In this chapter, you'll learn what a workflow hook is and how to consume them.
@@ -17440,450 +18112,6 @@ export async function POST(req: MedusaRequest, res: MedusaResponse) {
Your hook handler then receives that passed data in the `additional_data` object.
-# Workflow Timeout
-
-In this chapter, you’ll learn how to set a timeout for workflows and steps.
-
-## What is a Workflow Timeout?
-
-By default, a workflow doesn’t have a timeout. It continues execution until it’s finished or an error occurs.
-
-You can configure a workflow’s timeout to indicate how long the workflow can execute. If a workflow's execution time passes the configured timeout, it is failed and an error is thrown.
-
-### Timeout Doesn't Stop Step Execution
-
-Configuring a timeout doesn't stop the execution of a step in progress. The timeout only affects the status of the workflow and its result.
-
-***
-
-## Configure Workflow Timeout
-
-The `createWorkflow` function can accept a configuration object instead of the workflow’s name.
-
-In the configuration object, you pass a `timeout` property, whose value is a number indicating the timeout in seconds.
-
-For example:
-
-```ts title="src/workflows/hello-world.ts" highlights={[["16"]]} collapsibleLines="1-13" expandButtonLabel="Show More"
-import {
- createStep,
- createWorkflow,
- WorkflowResponse,
-} from "@medusajs/framework/workflows-sdk"
-
-const step1 = createStep(
- "step-1",
- async () => {
- // ...
- }
-)
-
-const myWorkflow = createWorkflow({
- name: "hello-world",
- timeout: 2, // 2 seconds
-}, function () {
- const str1 = step1()
-
- return new WorkflowResponse({
- message: str1,
- })
-})
-
-export default myWorkflow
-
-```
-
-This workflow's executions fail if they run longer than two seconds.
-
-A workflow’s timeout error is returned in the `errors` property of the workflow’s execution, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/errors/index.html.md). The error’s name is `TransactionTimeoutError`.
-
-***
-
-## Configure Step Timeout
-
-Alternatively, you can configure the timeout for a step rather than the entire workflow.
-
-As mentioned in the previous section, the timeout doesn't stop the execution of the step. It only affects the step's status and output.
-
-The step’s configuration object accepts a `timeout` property, whose value is a number indicating the timeout in seconds.
-
-For example:
-
-```tsx
-const step1 = createStep(
- {
- name: "step-1",
- timeout: 2, // 2 seconds
- },
- async () => {
- // ...
- }
-)
-```
-
-This step's executions fail if they run longer than two seconds.
-
-A step’s timeout error is returned in the `errors` property of the workflow’s execution, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/errors/index.html.md). The error’s name is `TransactionStepTimeoutError`.
-
-
-# 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.
-
-***
-
-## Documentation Content
-
-All documentation projects are built with Next.js. The content is writtin in MDX files.
-
-### Medusa Main Docs Content
-
-The content of the Medusa main docs are under the `www/apps/book/app` directory.
-
-### Medusa Resources Content
-
-The content of all pages under the `/resources` path are under the `www/apps/resources/app` directory.
-
-Documentation pages under the `www/apps/resources/references` directory are generated automatically from the source code under the `packages/medusa` directory. So, you can't directly make changes to them. Instead, you'll have to make changes to the comments in the original source code.
-
-### API Reference
-
-The API reference's content is split into two types:
-
-1. Static content, which are the content related to getting started, expanding fields, and more. These are located in the `www/apps/api-reference/markdown` directory. They are MDX files.
-2. OpenAPI specs that are shown to developers when checking the reference of an API Route. These are generated from OpenApi Spec comments, which are under the `www/utils/generated/oas-output` directory.
-
-### Medusa UI Documentation
-
-The content of the Medusa UI documentation are located under the `www/apps/ui/src/content/docs` directory. They are MDX files.
-
-The UI documentation also shows code examples, which are under the `www/apps/ui/src/examples` directory.
-
-The UI component props are generated from the source code and placed into the `www/apps/ui/src/specs` directory. To contribute to these props and their comments, check the comments in the source code under the `packages/design-system/ui` directory.
-
-***
-
-## Style Guide
-
-When you contribute to the documentation content, make sure to follow the [documentation style guide](https://www.notion.so/Style-Guide-Docs-fad86dd1c5f84b48b145e959f36628e0).
-
-***
-
-## How to Contribute
-
-If you’re fixing errors in an existing documentation page, you can scroll down to the end of the page and click on the “Edit this page” link. You’ll be redirected to the GitHub edit form of that page and you can make edits directly and submit a pull request (PR).
-
-If you’re adding a new page or contributing to the codebase, fork the repository, create a new branch, and make all changes necessary in your repository. Then, once you’re done, create a PR in the Medusa repository.
-
-### Base Branch
-
-When you make an edit to an existing documentation page or fork the repository to make changes to the documentation, create a new branch.
-
-Documentation contributions always use `develop` as the base branch. Make sure to also open your PR against the `develop` branch.
-
-### Branch Name
-
-Make sure that the branch name starts with `docs/`. For example, `docs/fix-services`. Vercel deployed previews are only triggered for branches starting with `docs/`.
-
-### Pull Request Conventions
-
-When you create a pull request, prefix the title with `docs:` or `docs(PROJECT_NAME):`, where `PROJECT_NAME` is the name of the documentation project this pull request pertains to. For example, `docs(ui): fix titles`.
-
-In the body of the PR, explain clearly what the PR does. If the PR solves an issue, use [closing keywords](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) with the issue number. For example, “Closes #1333”.
-
-***
-
-## Images
-
-If you are adding images to a documentation page, you can host the image on [Imgur](https://imgur.com) for free to include it in the PR. Our team will later upload it to our image hosting.
-
-***
-
-## NPM and Yarn Code Blocks
-
-If you’re adding code blocks that use NPM and Yarn, you must add the `npm2yarn` meta field.
-
-For example:
-
-````md
-```bash npm2yarn
-npm run start
-```
-````
-
-The code snippet must be written using NPM.
-
-### Global Option
-
-When a command uses the global option `-g`, add it at the end of the NPM command to ensure that it’s transformed to a Yarn command properly. For example:
-
-```bash npm2yarn
-npm install @medusajs/cli -g
-```
-
-***
-
-## Linting with Vale
-
-Medusa uses [Vale](https://vale.sh/) to lint documentation pages and perform checks on incoming PRs into the repository.
-
-### Result of Vale PR Checks
-
-You can check the result of running the "lint" action on your PR by clicking the Details link next to it. You can find there all errors that you need to fix.
-
-### Run Vale Locally
-
-If you want to check your work locally, you can do that by:
-
-1. [Installing Vale](https://vale.sh/docs/vale-cli/installation/) on your machine.
-2. Changing to the `www/vale` directory:
-
-```bash
-cd www/vale
-```
-
-3\. Running the `run-vale` script:
-
-```bash
-# to lint content for the main documentation
-./run-vale.sh book/app/learn error resources
-# to lint content for the resources documentation
-./run-vale.sh resources/app error
-# to lint content for the API reference
-./run-vale.sh api-reference/markdown error
-# to lint content for the Medusa UI documentation
-./run-vale.sh ui/src/content/docs error
-# to lint content for the user guide
-./run-vale.sh user-guide/app error
-```
-
-{/* TODO need to enable MDX v1 comments first. */}
-
-{/* ### Linter Exceptions
-
-If it's needed to break some style guide rules in a document, you can wrap the parts that the linter shouldn't scan with the following comments in the `md` or `mdx` files:
-
-```md
-
-
-content that shouldn't be scanned for errors here...
-
-
-```
-
-You can also disable specific rules. For example:
-
-```md
-
-
-Medusa supports Node versions 14 and 16.
-
-
-```
-
-If you use this in your PR, you must justify its usage. */}
-
-***
-
-## Linting with ESLint
-
-Medusa uses ESlint to lint code blocks both in the content and the code base of the documentation apps.
-
-### Linting Content with ESLint
-
-Each PR runs through a check that lints the code in the content files using ESLint. The action's name is `content-eslint`.
-
-If you want to check content ESLint errors locally and fix them, you can do that by:
-
-1\. Install the dependencies in the `www` directory:
-
-```bash
-yarn install
-```
-
-2\. Run the turbo command in the `www` directory:
-
-```bash
-turbo run lint:content
-```
-
-This will fix any fixable errors, and show errors that require your action.
-
-### Linting Code with ESLint
-
-Each PR runs through a check that lints the code in the content files using ESLint. The action's name is `code-docs-eslint`.
-
-If you want to check code ESLint errors locally and fix them, you can do that by:
-
-1\. Install the dependencies in the `www` directory:
-
-```bash
-yarn install
-```
-
-2\. Run the turbo command in the `www` directory:
-
-```bash
-yarn lint
-```
-
-This will fix any fixable errors, and show errors that require your action.
-
-{/* TODO need to enable MDX v1 comments first. */}
-
-{/* ### ESLint Exceptions
-
-If some code blocks have errors that can't or shouldn't be fixed, you can add the following command before the code block:
-
-~~~md
-
-
-```js
-console.log("This block isn't linted")
-```
-
-```js
-console.log("This block is linted")
-```
-~~~
-
-You can also disable specific rules. For example:
-
-~~~md
-
-
-```js
-console.log("This block can use semicolons");
-```
-
-```js
-console.log("This block can't use semi colons")
-```
-~~~ */}
-
-
# Example: Write Integration Tests for API Routes
In this chapter, you'll learn how to write integration tests for API routes using [medusaIntegrationTestRunner](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/integration-tests/index.html.md) from Medusa's Testing Framework.
@@ -18584,76 +18812,6 @@ The `errors` property contains an array of errors thrown during the execution of
If you threw a `MedusaError`, then you can check the error message in `errors[0].error.message`.
-# Example: Integration Tests for a Module
-
-In this chapter, find an example of writing an integration test for a module using [moduleIntegrationTestRunner](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/modules-tests/index.html.md) from Medusa's Testing Framework.
-
-### Prerequisites
-
-- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md)
-
-## Write Integration Test for Module
-
-Consider a `blog` module with a `BlogModuleService` that has a `getMessage` method:
-
-```ts title="src/modules/blog/service.ts"
-import { MedusaService } from "@medusajs/framework/utils"
-import MyCustom from "./models/my-custom"
-
-class BlogModuleService extends MedusaService({
- MyCustom,
-}){
- getMessage(): string {
- return "Hello, World!"
- }
-}
-
-export default BlogModuleService
-```
-
-To create an integration test for the method, create the file `src/modules/blog/__tests__/service.spec.ts` with the following content:
-
-```ts title="src/modules/blog/__tests__/service.spec.ts"
-import { moduleIntegrationTestRunner } from "@medusajs/test-utils"
-import { BLOG_MODULE } from ".."
-import BlogModuleService from "../service"
-import MyCustom from "../models/my-custom"
-
-moduleIntegrationTestRunner({
- moduleName: BLOG_MODULE,
- moduleModels: [MyCustom],
- resolve: "./src/modules/blog",
- testSuite: ({ service }) => {
- describe("BlogModuleService", () => {
- it("says hello world", () => {
- const message = service.getMessage()
-
- expect(message).toEqual("Hello, World!")
- })
- })
- },
-})
-
-jest.setTimeout(60 * 1000)
-```
-
-You use the `moduleIntegrationTestRunner` function to add tests for the `blog` module. You have one test that passes if the `getMessage` method returns the `"Hello, World!"` string.
-
-***
-
-## Run Test
-
-Run the following command to run your module integration tests:
-
-```bash npm2yarn
-npm run test:integration:modules
-```
-
-If you don't have a `test:integration:modules` script in `package.json`, refer to the [Medusa Testing Tools chapter](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools#add-test-commands/index.html.md).
-
-This runs your Medusa application and runs the tests available in any `__tests__` directory under the `src/modules` directory.
-
-
# Commerce Modules
In this section of the documentation, you'll find guides and references related to Medusa's Commerce Modules.
@@ -18834,136 +18992,6 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc
***
-# Auth Module
-
-In this section of the documentation, you will find resources to learn more about the Auth Module and how to use it in your application.
-
-Medusa has auth related features available out-of-the-box through the Auth Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Auth Module.
-
-Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).
-
-## Auth Features
-
-- [Basic User Authentication](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#1-basic-authentication-flow/index.html.md): Authenticate users using their email and password credentials.
-- [Third-Party and Social Authentication](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#2-third-party-service-authenticate-flow/index.html.md): Authenticate users using third-party services and social platforms, such as [Google](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/google/index.html.md) and [GitHub](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/github/index.html.md).
-- [Authenticate Custom Actor Types](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/create-actor-type/index.html.md): Create custom user or actor types, such as managers, authenticate them in your application, and guard routes based on the custom user types.
-- [Custom Authentication Providers](https://docs.medusajs.com/references/auth/provider/index.html.md): Integrate third-party services with custom authentication providors.
-
-***
-
-## How to Use the Auth Module
-
-In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.
-
-You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.
-
-For example:
-
-```ts title="src/workflows/authenticate-user.ts" highlights={highlights}
-import {
- createWorkflow,
- WorkflowResponse,
- createStep,
- StepResponse,
-} from "@medusajs/framework/workflows-sdk"
-import { Modules, MedusaError } from "@medusajs/framework/utils"
-import { MedusaRequest } from "@medusajs/framework/http"
-import { AuthenticationInput } from "@medusajs/framework/types"
-
-type Input = {
- req: MedusaRequest
-}
-
-const authenticateUserStep = createStep(
- "authenticate-user",
- async ({ req }: Input, { container }) => {
- const authModuleService = container.resolve(Modules.AUTH)
-
- const { success, authIdentity, error } = await authModuleService
- .authenticate(
- "emailpass",
- {
- url: req.url,
- headers: req.headers,
- query: req.query,
- body: req.body,
- authScope: "admin", // or custom actor type
- protocol: req.protocol,
- } as AuthenticationInput
- )
-
- if (!success) {
- // incorrect authentication details
- throw new MedusaError(
- MedusaError.Types.UNAUTHORIZED,
- error || "Incorrect authentication details"
- )
- }
-
- return new StepResponse({ authIdentity }, authIdentity?.id)
- },
- async (authIdentityId, { container }) => {
- if (!authIdentityId) {
- return
- }
-
- const authModuleService = container.resolve(Modules.AUTH)
-
- await authModuleService.deleteAuthIdentities([authIdentityId])
- }
-)
-
-export const authenticateUserWorkflow = createWorkflow(
- "authenticate-user",
- (input: Input) => {
- const { authIdentity } = authenticateUserStep(input)
-
- return new WorkflowResponse({
- authIdentity,
- })
- }
-)
-```
-
-You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:
-
-```ts title="API Route" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
-import type {
- MedusaRequest,
- MedusaResponse,
-} from "@medusajs/framework/http"
-import { authenticateUserWorkflow } from "../../workflows/authenticate-user"
-
-export async function GET(
- req: MedusaRequest,
- res: MedusaResponse
-) {
- const { result } = await authenticateUserWorkflow(req.scope)
- .run({
- req,
- })
-
- res.send(result)
-}
-```
-
-Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).
-
-***
-
-## Configure Auth Module
-
-The Auth Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/module-options/index.html.md) for details on the module's options.
-
-***
-
-## Providers
-
-Medusa provides the following authentication providers out-of-the-box. You can use them to authenticate admin users, customers, or custom actor types.
-
-***
-
-
# 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.
@@ -19114,24 +19142,24 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc
***
-# Customer Module
+# Auth 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 Auth Module and how to use it in your application.
-Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/customers/index.html.md) to learn how to manage customers and groups using the dashboard.
-
-Medusa has customer related features available out-of-the-box through the Customer Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Customer Module.
+Medusa has auth related features available out-of-the-box through the Auth Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Auth Module.
Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).
-## Customer Features
+## Auth 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).
+- [Basic User Authentication](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#1-basic-authentication-flow/index.html.md): Authenticate users using their email and password credentials.
+- [Third-Party and Social Authentication](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#2-third-party-service-authenticate-flow/index.html.md): Authenticate users using third-party services and social platforms, such as [Google](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/google/index.html.md) and [GitHub](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/github/index.html.md).
+- [Authenticate Custom Actor Types](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/create-actor-type/index.html.md): Create custom user or actor types, such as managers, authenticate them in your application, and guard routes based on the custom user types.
+- [Custom Authentication Providers](https://docs.medusajs.com/references/auth/provider/index.html.md): Integrate third-party services with custom authentication providors.
***
-## How to Use the Customer Module
+## How to Use the Auth Module
In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.
@@ -19139,45 +19167,67 @@ 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/authenticate-user.ts" highlights={highlights}
import {
createWorkflow,
WorkflowResponse,
createStep,
StepResponse,
} from "@medusajs/framework/workflows-sdk"
-import { Modules } from "@medusajs/framework/utils"
+import { Modules, MedusaError } from "@medusajs/framework/utils"
+import { MedusaRequest } from "@medusajs/framework/http"
+import { AuthenticationInput } from "@medusajs/framework/types"
-const createCustomerStep = createStep(
- "create-customer",
- async ({}, { container }) => {
- const customerModuleService = container.resolve(Modules.CUSTOMER)
+type Input = {
+ req: MedusaRequest
+}
- const customer = await customerModuleService.createCustomers({
- first_name: "Peter",
- last_name: "Hayes",
- email: "peter.hayes@example.com",
- })
+const authenticateUserStep = createStep(
+ "authenticate-user",
+ async ({ req }: Input, { container }) => {
+ const authModuleService = container.resolve(Modules.AUTH)
- return new StepResponse({ customer }, customer.id)
+ const { success, authIdentity, error } = await authModuleService
+ .authenticate(
+ "emailpass",
+ {
+ url: req.url,
+ headers: req.headers,
+ query: req.query,
+ body: req.body,
+ authScope: "admin", // or custom actor type
+ protocol: req.protocol,
+ } as AuthenticationInput
+ )
+
+ if (!success) {
+ // incorrect authentication details
+ throw new MedusaError(
+ MedusaError.Types.UNAUTHORIZED,
+ error || "Incorrect authentication details"
+ )
+ }
+
+ return new StepResponse({ authIdentity }, authIdentity?.id)
},
- async (customerId, { container }) => {
- if (!customerId) {
+ async (authIdentityId, { container }) => {
+ if (!authIdentityId) {
return
}
- const customerModuleService = container.resolve(Modules.CUSTOMER)
+
+ const authModuleService = container.resolve(Modules.AUTH)
- await customerModuleService.deleteCustomers([customerId])
+ await authModuleService.deleteAuthIdentities([authIdentityId])
}
)
-export const createCustomerWorkflow = createWorkflow(
- "create-customer",
- () => {
- const { customer } = createCustomerStep()
+export const authenticateUserWorkflow = createWorkflow(
+ "authenticate-user",
+ (input: Input) => {
+ const { authIdentity } = authenticateUserStep(input)
return new WorkflowResponse({
- customer,
+ authIdentity,
})
}
)
@@ -19185,216 +19235,39 @@ export const createCustomerWorkflow = createWorkflow(
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"
+```ts title="API Route" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
-import { createCustomerWorkflow } from "../../workflows/create-customer"
+import { authenticateUserWorkflow } from "../../workflows/authenticate-user"
export async function GET(
req: MedusaRequest,
res: MedusaResponse
) {
- const { result } = await createCustomerWorkflow(req.scope)
- .run()
+ const { result } = await authenticateUserWorkflow(req.scope)
+ .run({
+ req,
+ })
res.send(result)
}
```
-### Subscriber
-
-```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
-import {
- type SubscriberConfig,
- type SubscriberArgs,
-} from "@medusajs/framework"
-import { createCustomerWorkflow } from "../workflows/create-customer"
-
-export default async function handleUserCreated({
- event: { data },
- container,
-}: SubscriberArgs<{ id: string }>) {
- const { result } = await createCustomerWorkflow(container)
- .run()
-
- console.log(result)
-}
-
-export const config: SubscriberConfig = {
- event: "user.created",
-}
-```
-
-### Scheduled Job
-
-```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
-import { MedusaContainer } from "@medusajs/framework/types"
-import { createCustomerWorkflow } from "../workflows/create-customer"
-
-export default async function myCustomJob(
- container: MedusaContainer
-) {
- const { result } = await createCustomerWorkflow(container)
- .run()
-
- console.log(result)
-}
-
-export const config = {
- name: "run-once-a-day",
- schedule: `0 0 * * *`,
-}
-```
-
Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).
***
+## Configure Auth Module
-# 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.
+The Auth Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/module-options/index.html.md) for details on the module's options.
***
-## How to Use the Inventory Module
+## Providers
-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).
+Medusa provides the following authentication providers out-of-the-box. You can use them to authenticate admin users, customers, or custom actor types.
***
@@ -19566,6 +19439,150 @@ The Fulfillment Module accepts options for further configurations. Refer to [thi
***
+# Inventory Module
+
+In this section of the documentation, you will find resources to learn more about the Inventory Module and how to use it in your application.
+
+Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/inventory/index.html.md) to learn how to manage inventory and related features using the dashboard.
+
+Medusa has inventory related features available out-of-the-box through the Inventory Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Inventory Module.
+
+Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).
+
+## Inventory Features
+
+- [Inventory Items Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts/index.html.md): Store and manage inventory of any stock-kept item, such as product variants.
+- [Inventory Across Locations](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts#inventorylevel/index.html.md): Manage inventory levels across different locations, such as warehouses.
+- [Reservation Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts#reservationitem/index.html.md): Reserve quantities of inventory items at specific locations for orders or other purposes.
+- [Check Inventory Availability](https://docs.medusajs.com/references/inventory-next/confirmInventory/index.html.md): Check whether an inventory item has the necessary quantity for purchase.
+- [Inventory Kits](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-kit/index.html.md): Create and manage inventory kits for a single product, allowing you to implement use cases like bundled or multi-part products.
+
+***
+
+## How to Use the Inventory Module
+
+In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.
+
+You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.
+
+For example:
+
+```ts title="src/workflows/create-inventory-item.ts" highlights={highlights}
+import {
+ createWorkflow,
+ WorkflowResponse,
+ createStep,
+ StepResponse,
+} from "@medusajs/framework/workflows-sdk"
+import { Modules } from "@medusajs/framework/utils"
+
+const createInventoryItemStep = createStep(
+ "create-inventory-item",
+ async ({}, { container }) => {
+ const inventoryModuleService = container.resolve(Modules.INVENTORY)
+
+ const inventoryItem = await inventoryModuleService.createInventoryItems({
+ sku: "SHIRT",
+ title: "Green Medusa Shirt",
+ requires_shipping: true,
+ })
+
+ return new StepResponse({ inventoryItem }, inventoryItem.id)
+ },
+ async (inventoryItemId, { container }) => {
+ if (!inventoryItemId) {
+ return
+ }
+ const inventoryModuleService = container.resolve(Modules.INVENTORY)
+
+ await inventoryModuleService.deleteInventoryItems([inventoryItemId])
+ }
+)
+
+export const createInventoryItemWorkflow = createWorkflow(
+ "create-inventory-item-workflow",
+ () => {
+ const { inventoryItem } = createInventoryItemStep()
+
+ return new WorkflowResponse({
+ inventoryItem,
+ })
+ }
+)
+```
+
+You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:
+
+### API Route
+
+```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
+import type {
+ MedusaRequest,
+ MedusaResponse,
+} from "@medusajs/framework/http"
+import { createInventoryItemWorkflow } from "../../workflows/create-inventory-item"
+
+export async function GET(
+ req: MedusaRequest,
+ res: MedusaResponse
+) {
+ const { result } = await createInventoryItemWorkflow(req.scope)
+ .run()
+
+ res.send(result)
+}
+```
+
+### Subscriber
+
+```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
+import {
+ type SubscriberConfig,
+ type SubscriberArgs,
+} from "@medusajs/framework"
+import { createInventoryItemWorkflow } from "../workflows/create-inventory-item"
+
+export default async function handleUserCreated({
+ event: { data },
+ container,
+}: SubscriberArgs<{ id: string }>) {
+ const { result } = await createInventoryItemWorkflow(container)
+ .run()
+
+ console.log(result)
+}
+
+export const config: SubscriberConfig = {
+ event: "user.created",
+}
+```
+
+### Scheduled Job
+
+```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
+import { MedusaContainer } from "@medusajs/framework/types"
+import { createInventoryItemWorkflow } from "../workflows/create-inventory-item"
+
+export default async function myCustomJob(
+ container: MedusaContainer
+) {
+ const { result } = await createInventoryItemWorkflow(container)
+ .run()
+
+ console.log(result)
+}
+
+export const config = {
+ name: "run-once-a-day",
+ schedule: `0 0 * * *`,
+}
+```
+
+Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).
+
+***
+
+
# Order Module
In this section of the documentation, you will find resources to learn more about the Order Module and how to use it in your application.
@@ -19870,27 +19887,24 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc
***
-# Payment Module
+# Customer 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.
+In this section of the documentation, you will find resources to learn more about the Customer Module and how to use it in your application.
-Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/payments/index.html.md) to learn how to manage order payments using the dashboard.
+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 payment related features available out-of-the-box through the Payment Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Payment Module.
+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).
-## Payment Features
+## Customer Features
-- [Authorize, Capture, and Refund Payments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment/index.html.md): Authorize, capture, and refund payments for a single resource.
-- [Payment Collection Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-collection/index.html.md): Store and manage all payments of a single resources, such as a cart, in payment collections.
-- [Integrate Third-Party Payment Providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/index.html.md): Use payment providers like [Stripe](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md) to handle and process payments, or integrate custom payment providers.
-- [Saved Payment Methods](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/account-holder/index.html.md): Save payment methods for customers in third-party payment providers.
-- [Handle Webhook Events](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/webhook-events/index.html.md): Handle webhook events from third-party providers and process the associated payment.
+- [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 Payment 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.
@@ -19898,7 +19912,7 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows
For example:
-```ts title="src/workflows/create-payment-collection.ts" highlights={highlights}
+```ts title="src/workflows/create-customer.ts" highlights={highlights}
import {
createWorkflow,
WorkflowResponse,
@@ -19907,35 +19921,36 @@ import {
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"
-const createPaymentCollectionStep = createStep(
- "create-payment-collection",
+const createCustomerStep = createStep(
+ "create-customer",
async ({}, { container }) => {
- const paymentModuleService = container.resolve(Modules.PAYMENT)
+ const customerModuleService = container.resolve(Modules.CUSTOMER)
- const paymentCollection = await paymentModuleService.createPaymentCollections({
- currency_code: "usd",
- amount: 5000,
+ const customer = await customerModuleService.createCustomers({
+ first_name: "Peter",
+ last_name: "Hayes",
+ email: "peter.hayes@example.com",
})
- return new StepResponse({ paymentCollection }, paymentCollection.id)
+ return new StepResponse({ customer }, customer.id)
},
- async (paymentCollectionId, { container }) => {
- if (!paymentCollectionId) {
+ async (customerId, { container }) => {
+ if (!customerId) {
return
}
- const paymentModuleService = container.resolve(Modules.PAYMENT)
+ const customerModuleService = container.resolve(Modules.CUSTOMER)
- await paymentModuleService.deletePaymentCollections([paymentCollectionId])
+ await customerModuleService.deleteCustomers([customerId])
}
)
-export const createPaymentCollectionWorkflow = createWorkflow(
- "create-payment-collection",
+export const createCustomerWorkflow = createWorkflow(
+ "create-customer",
() => {
- const { paymentCollection } = createPaymentCollectionStep()
+ const { customer } = createCustomerStep()
return new WorkflowResponse({
- paymentCollection,
+ customer,
})
}
)
@@ -19950,13 +19965,13 @@ import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
-import { createPaymentCollectionWorkflow } from "../../workflows/create-payment-collection"
+import { createCustomerWorkflow } from "../../workflows/create-customer"
export async function GET(
req: MedusaRequest,
res: MedusaResponse
) {
- const { result } = await createPaymentCollectionWorkflow(req.scope)
+ const { result } = await createCustomerWorkflow(req.scope)
.run()
res.send(result)
@@ -19970,13 +19985,13 @@ import {
type SubscriberConfig,
type SubscriberArgs,
} from "@medusajs/framework"
-import { createPaymentCollectionWorkflow } from "../workflows/create-payment-collection"
+import { createCustomerWorkflow } from "../workflows/create-customer"
export default async function handleUserCreated({
event: { data },
container,
}: SubscriberArgs<{ id: string }>) {
- const { result } = await createPaymentCollectionWorkflow(container)
+ const { result } = await createCustomerWorkflow(container)
.run()
console.log(result)
@@ -19991,12 +20006,12 @@ export const config: SubscriberConfig = {
```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
import { MedusaContainer } from "@medusajs/framework/types"
-import { createPaymentCollectionWorkflow } from "../workflows/create-payment-collection"
+import { createCustomerWorkflow } from "../workflows/create-customer"
export default async function myCustomJob(
container: MedusaContainer
) {
- const { result } = await createPaymentCollectionWorkflow(container)
+ const { result } = await createCustomerWorkflow(container)
.run()
console.log(result)
@@ -20012,18 +20027,6 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc
***
-## Configure Payment Module
-
-The Payment Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/module-options/index.html.md) for details on the module's options.
-
-***
-
-## Providers
-
-Medusa provides the following payment providers out-of-the-box. You can use them to process payments for orders, returns, and other resources.
-
-***
-
# Pricing Module
@@ -20179,27 +20182,27 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc
***
-# Region Module
+# Payment Module
-In this section of the documentation, you will find resources to learn more about the Region Module and how to use it in your application.
+In this section of the documentation, you will find resources to learn more about the Payment Module and how to use it in your application.
-Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/regions/index.html.md) to learn how to manage regions using the dashboard.
+Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/payments/index.html.md) to learn how to manage order payments using the dashboard.
-Medusa has region related features available out-of-the-box through the Region Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Region Module.
+Medusa has payment related features available out-of-the-box through the Payment Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Payment Module.
Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).
-***
+## Payment Features
-## Region Features
-
-- [Region Management](https://docs.medusajs.com/references/region/models/Region/index.html.md): Manage regions in your store. You can create regions with different currencies and settings.
-- [Multi-Currency Support](https://docs.medusajs.com/references/region/models/Region/index.html.md): Each region has a currency. You can support multiple currencies in your store by creating multiple regions.
-- [Different Settings Per Region](https://docs.medusajs.com/references/region/models/Region/index.html.md): Each region has its own settings, such as what countries belong to a region or its tax settings.
+- [Authorize, Capture, and Refund Payments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment/index.html.md): Authorize, capture, and refund payments for a single resource.
+- [Payment Collection Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-collection/index.html.md): Store and manage all payments of a single resources, such as a cart, in payment collections.
+- [Integrate Third-Party Payment Providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/index.html.md): Use payment providers like [Stripe](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md) to handle and process payments, or integrate custom payment providers.
+- [Saved Payment Methods](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/account-holder/index.html.md): Save payment methods for customers in third-party payment providers.
+- [Handle Webhook Events](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/webhook-events/index.html.md): Handle webhook events from third-party providers and process the associated payment.
***
-## How to Use Region Module's Service
+## How to Use the Payment Module
In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.
@@ -20207,7 +20210,7 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows
For example:
-```ts title="src/workflows/create-region.ts" highlights={highlights}
+```ts title="src/workflows/create-payment-collection.ts" highlights={highlights}
import {
createWorkflow,
WorkflowResponse,
@@ -20216,35 +20219,35 @@ import {
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"
-const createRegionStep = createStep(
- "create-region",
+const createPaymentCollectionStep = createStep(
+ "create-payment-collection",
async ({}, { container }) => {
- const regionModuleService = container.resolve(Modules.REGION)
+ const paymentModuleService = container.resolve(Modules.PAYMENT)
- const region = await regionModuleService.createRegions({
- name: "Europe",
- currency_code: "eur",
+ const paymentCollection = await paymentModuleService.createPaymentCollections({
+ currency_code: "usd",
+ amount: 5000,
})
- return new StepResponse({ region }, region.id)
+ return new StepResponse({ paymentCollection }, paymentCollection.id)
},
- async (regionId, { container }) => {
- if (!regionId) {
+ async (paymentCollectionId, { container }) => {
+ if (!paymentCollectionId) {
return
}
- const regionModuleService = container.resolve(Modules.REGION)
+ const paymentModuleService = container.resolve(Modules.PAYMENT)
- await regionModuleService.deleteRegions([regionId])
+ await paymentModuleService.deletePaymentCollections([paymentCollectionId])
}
)
-export const createRegionWorkflow = createWorkflow(
- "create-region",
+export const createPaymentCollectionWorkflow = createWorkflow(
+ "create-payment-collection",
() => {
- const { region } = createRegionStep()
+ const { paymentCollection } = createPaymentCollectionStep()
return new WorkflowResponse({
- region,
+ paymentCollection,
})
}
)
@@ -20259,13 +20262,13 @@ import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
-import { createRegionWorkflow } from "../../workflows/create-region"
+import { createPaymentCollectionWorkflow } from "../../workflows/create-payment-collection"
export async function GET(
req: MedusaRequest,
res: MedusaResponse
) {
- const { result } = await createRegionWorkflow(req.scope)
+ const { result } = await createPaymentCollectionWorkflow(req.scope)
.run()
res.send(result)
@@ -20279,13 +20282,13 @@ import {
type SubscriberConfig,
type SubscriberArgs,
} from "@medusajs/framework"
-import { createRegionWorkflow } from "../workflows/create-region"
+import { createPaymentCollectionWorkflow } from "../workflows/create-payment-collection"
export default async function handleUserCreated({
event: { data },
container,
}: SubscriberArgs<{ id: string }>) {
- const { result } = await createRegionWorkflow(container)
+ const { result } = await createPaymentCollectionWorkflow(container)
.run()
console.log(result)
@@ -20300,12 +20303,12 @@ export const config: SubscriberConfig = {
```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
import { MedusaContainer } from "@medusajs/framework/types"
-import { createRegionWorkflow } from "../workflows/create-region"
+import { createPaymentCollectionWorkflow } from "../workflows/create-payment-collection"
export default async function myCustomJob(
container: MedusaContainer
) {
- const { result } = await createRegionWorkflow(container)
+ const { result } = await createPaymentCollectionWorkflow(container)
.run()
console.log(result)
@@ -20321,6 +20324,18 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc
***
+## Configure Payment Module
+
+The Payment Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/module-options/index.html.md) for details on the module's options.
+
+***
+
+## Providers
+
+Medusa provides the following payment providers out-of-the-box. You can use them to process payments for orders, returns, and other resources.
+
+***
+
# Product Module
@@ -20477,24 +20492,26 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc
***
-# Stock Location Module
+# Promotion Module
-In this section of the documentation, you will find resources to learn more about the Stock Location Module and how to use it in your application.
+In this section of the documentation, you will find resources to learn more about the Promotion Module and how to use it in your application.
-Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/index.html.md) to learn how to manage stock locations using the dashboard.
+Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/promotions/index.html.md) to learn how to manage promotions using the dashboard.
-Medusa has stock location related features available out-of-the-box through the Stock Location Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Stock Location Module.
+Medusa has 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).
-## Stock Location Features
+## Promotion Features
-- [Stock Location Management](https://docs.medusajs.com/references/stock-location-next/models/index.html.md): Store and manage stock locations. Medusa links stock locations with data models of other modules that require a location, such as the [Inventory Module's InventoryLevel](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/links-to-other-modules/index.html.md).
-- [Address Management](https://docs.medusajs.com/references/stock-location-next/models/StockLocationAddress/index.html.md): Manage the address of each stock location.
+- [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 Stock Location Module's Service
+## 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.
@@ -20502,7 +20519,7 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows
For example:
-```ts title="src/workflows/create-stock-location.ts" highlights={highlights}
+```ts title="src/workflows/create-promotion.ts" highlights={highlights}
import {
createWorkflow,
WorkflowResponse,
@@ -20511,33 +20528,42 @@ import {
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"
-const createStockLocationStep = createStep(
- "create-stock-location",
+const createPromotionStep = createStep(
+ "create-promotion",
async ({}, { container }) => {
- const stockLocationModuleService = container.resolve(Modules.STOCK_LOCATION)
+ const promotionModuleService = container.resolve(Modules.PROMOTION)
- const stockLocation = await stockLocationModuleService.createStockLocations({
- name: "Warehouse 1",
+ 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({ stockLocation }, stockLocation.id)
+ return new StepResponse({ promotion }, promotion.id)
},
- async (stockLocationId, { container }) => {
- if (!stockLocationId) {
+ async (promotionId, { container }) => {
+ if (!promotionId) {
return
}
- const stockLocationModuleService = container.resolve(Modules.STOCK_LOCATION)
+ const promotionModuleService = container.resolve(Modules.PROMOTION)
- await stockLocationModuleService.deleteStockLocations([stockLocationId])
+ await promotionModuleService.deletePromotions(promotionId)
}
)
-export const createStockLocationWorkflow = createWorkflow(
- "create-stock-location",
+export const createPromotionWorkflow = createWorkflow(
+ "create-promotion",
() => {
- const { stockLocation } = createStockLocationStep()
+ const { promotion } = createPromotionStep()
- return new WorkflowResponse({ stockLocation })
+ return new WorkflowResponse({
+ promotion,
+ })
}
)
```
@@ -20551,13 +20577,13 @@ import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
-import { createStockLocationWorkflow } from "../../workflows/create-stock-location"
+import { createPromotionWorkflow } from "../../workflows/create-cart"
export async function GET(
req: MedusaRequest,
res: MedusaResponse
) {
- const { result } = await createStockLocationWorkflow(req.scope)
+ const { result } = await createPromotionWorkflow(req.scope)
.run()
res.send(result)
@@ -20571,13 +20597,13 @@ import {
type SubscriberConfig,
type SubscriberArgs,
} from "@medusajs/framework"
-import { createStockLocationWorkflow } from "../workflows/create-stock-location"
+import { createPromotionWorkflow } from "../workflows/create-cart"
export default async function handleUserCreated({
event: { data },
container,
}: SubscriberArgs<{ id: string }>) {
- const { result } = await createStockLocationWorkflow(container)
+ const { result } = await createPromotionWorkflow(container)
.run()
console.log(result)
@@ -20592,12 +20618,12 @@ export const config: SubscriberConfig = {
```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
import { MedusaContainer } from "@medusajs/framework/types"
-import { createStockLocationWorkflow } from "../workflows/create-stock-location"
+import { createPromotionWorkflow } from "../workflows/create-cart"
export default async function myCustomJob(
container: MedusaContainer
) {
- const { result } = await createStockLocationWorkflow(container)
+ const { result } = await createPromotionWorkflow(container)
.run()
console.log(result)
@@ -20774,6 +20800,286 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc
***
+# Region Module
+
+In this section of the documentation, you will find resources to learn more about the Region Module and how to use it in your application.
+
+Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/regions/index.html.md) to learn how to manage regions using the dashboard.
+
+Medusa has region related features available out-of-the-box through the Region Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Region Module.
+
+Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).
+
+***
+
+## Region Features
+
+- [Region Management](https://docs.medusajs.com/references/region/models/Region/index.html.md): Manage regions in your store. You can create regions with different currencies and settings.
+- [Multi-Currency Support](https://docs.medusajs.com/references/region/models/Region/index.html.md): Each region has a currency. You can support multiple currencies in your store by creating multiple regions.
+- [Different Settings Per Region](https://docs.medusajs.com/references/region/models/Region/index.html.md): Each region has its own settings, such as what countries belong to a region or its tax settings.
+
+***
+
+## How to Use Region Module's Service
+
+In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.
+
+You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.
+
+For example:
+
+```ts title="src/workflows/create-region.ts" highlights={highlights}
+import {
+ createWorkflow,
+ WorkflowResponse,
+ createStep,
+ StepResponse,
+} from "@medusajs/framework/workflows-sdk"
+import { Modules } from "@medusajs/framework/utils"
+
+const createRegionStep = createStep(
+ "create-region",
+ async ({}, { container }) => {
+ const regionModuleService = container.resolve(Modules.REGION)
+
+ const region = await regionModuleService.createRegions({
+ name: "Europe",
+ currency_code: "eur",
+ })
+
+ return new StepResponse({ region }, region.id)
+ },
+ async (regionId, { container }) => {
+ if (!regionId) {
+ return
+ }
+ const regionModuleService = container.resolve(Modules.REGION)
+
+ await regionModuleService.deleteRegions([regionId])
+ }
+)
+
+export const createRegionWorkflow = createWorkflow(
+ "create-region",
+ () => {
+ const { region } = createRegionStep()
+
+ return new WorkflowResponse({
+ region,
+ })
+ }
+)
+```
+
+You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:
+
+### API Route
+
+```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
+import type {
+ MedusaRequest,
+ MedusaResponse,
+} from "@medusajs/framework/http"
+import { createRegionWorkflow } from "../../workflows/create-region"
+
+export async function GET(
+ req: MedusaRequest,
+ res: MedusaResponse
+) {
+ const { result } = await createRegionWorkflow(req.scope)
+ .run()
+
+ res.send(result)
+}
+```
+
+### Subscriber
+
+```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
+import {
+ type SubscriberConfig,
+ type SubscriberArgs,
+} from "@medusajs/framework"
+import { createRegionWorkflow } from "../workflows/create-region"
+
+export default async function handleUserCreated({
+ event: { data },
+ container,
+}: SubscriberArgs<{ id: string }>) {
+ const { result } = await createRegionWorkflow(container)
+ .run()
+
+ console.log(result)
+}
+
+export const config: SubscriberConfig = {
+ event: "user.created",
+}
+```
+
+### Scheduled Job
+
+```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
+import { MedusaContainer } from "@medusajs/framework/types"
+import { createRegionWorkflow } from "../workflows/create-region"
+
+export default async function myCustomJob(
+ container: MedusaContainer
+) {
+ const { result } = await createRegionWorkflow(container)
+ .run()
+
+ console.log(result)
+}
+
+export const config = {
+ name: "run-once-a-day",
+ schedule: `0 0 * * *`,
+}
+```
+
+Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).
+
+***
+
+
+# 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.
+
+Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/index.html.md) to learn how to manage stock locations using the dashboard.
+
+Medusa has stock location related features available out-of-the-box through the Stock Location Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Stock Location Module.
+
+Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).
+
+## Stock Location Features
+
+- [Stock Location Management](https://docs.medusajs.com/references/stock-location-next/models/index.html.md): Store and manage stock locations. Medusa links stock locations with data models of other modules that require a location, such as the [Inventory Module's InventoryLevel](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/links-to-other-modules/index.html.md).
+- [Address Management](https://docs.medusajs.com/references/stock-location-next/models/StockLocationAddress/index.html.md): Manage the address of each stock location.
+
+***
+
+## How to Use Stock Location Module's Service
+
+In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.
+
+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-stock-location.ts" highlights={highlights}
+import {
+ createWorkflow,
+ WorkflowResponse,
+ createStep,
+ StepResponse,
+} from "@medusajs/framework/workflows-sdk"
+import { Modules } from "@medusajs/framework/utils"
+
+const createStockLocationStep = createStep(
+ "create-stock-location",
+ async ({}, { container }) => {
+ const stockLocationModuleService = container.resolve(Modules.STOCK_LOCATION)
+
+ const stockLocation = await stockLocationModuleService.createStockLocations({
+ name: "Warehouse 1",
+ })
+
+ return new StepResponse({ stockLocation }, stockLocation.id)
+ },
+ async (stockLocationId, { container }) => {
+ if (!stockLocationId) {
+ return
+ }
+ const stockLocationModuleService = container.resolve(Modules.STOCK_LOCATION)
+
+ await stockLocationModuleService.deleteStockLocations([stockLocationId])
+ }
+)
+
+export const createStockLocationWorkflow = createWorkflow(
+ "create-stock-location",
+ () => {
+ const { stockLocation } = createStockLocationStep()
+
+ return new WorkflowResponse({ stockLocation })
+ }
+)
+```
+
+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 { createStockLocationWorkflow } from "../../workflows/create-stock-location"
+
+export async function GET(
+ req: MedusaRequest,
+ res: MedusaResponse
+) {
+ const { result } = await createStockLocationWorkflow(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 { createStockLocationWorkflow } from "../workflows/create-stock-location"
+
+export default async function handleUserCreated({
+ event: { data },
+ container,
+}: SubscriberArgs<{ id: string }>) {
+ const { result } = await createStockLocationWorkflow(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 { createStockLocationWorkflow } from "../workflows/create-stock-location"
+
+export default async function myCustomJob(
+ container: MedusaContainer
+) {
+ const { result } = await createStockLocationWorkflow(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).
+
+***
+
+
# 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.
@@ -21235,223 +21541,6 @@ The associated token is no longer usable or verifiable.
To verify a token received as an input or in a request, use the [authenticate method of the module’s main service](https://docs.medusajs.com/references/api-key/authenticate/index.html.md) which validates the token against all non-expired tokens.
-# 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).
-
-***
-
-
-# Auth Identity and Actor Types
-
-In this document, you’ll learn about concepts related to identity and actors in the Auth Module.
-
-## What is an Auth Identity?
-
-The [AuthIdentity data model](https://docs.medusajs.com/references/auth/models/AuthIdentity/index.html.md) represents a user registered by an [authentication provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/index.html.md). When a user is registered using an authentication provider, the provider creates a record of `AuthIdentity`.
-
-Then, when the user logs-in in the future with the same authentication provider, the associated auth identity is used to validate their credentials.
-
-***
-
-## Actor Types
-
-An actor type is a type of user that can be authenticated. The Auth Module doesn't store or manage any user-like models, such as for customers or users. Instead, the user types are created and managed by other modules. For example, a customer is managed by the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md).
-
-Then, when an auth identity is created for the actor type, the ID of the user is stored in the `app_metadata` property of the auth identity.
-
-For example, an auth identity of a customer has the following `app_metadata` property:
-
-```json
-{
- "app_metadata": {
- "customer_id": "cus_123"
- }
-}
-```
-
-The ID of the user is stored in the key `{actor_type}_id` of the `app_metadata` property.
-
-***
-
-## Protect Routes by Actor Type
-
-When you protect routes with the `authenticate` middleware, you specify in its first parameter the actor type that must be authenticated to access the specified API routes.
-
-For example:
-
-```ts title="src/api/middlewares.ts" highlights={highlights}
-import {
- defineMiddlewares,
- authenticate,
-} from "@medusajs/framework/http"
-
-export default defineMiddlewares({
- routes: [
- {
- matcher: "/custom/admin*",
- middlewares: [
- authenticate("user", ["session", "bearer", "api-key"]),
- ],
- },
- ],
-})
-```
-
-By specifying `user` as the first parameter of `authenticate`, only authenticated users of actor type `user` (admin users) can access API routes starting with `/custom/admin`.
-
-***
-
-## Custom Actor Types
-
-You can define custom actor types that allows a custom user, managed by your custom module, to authenticate into Medusa.
-
-For example, if you have a custom module with a `Manager` data model, you can authenticate managers with the `manager` actor type.
-
-Learn how to create a custom actor type in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/create-actor-type/index.html.md).
-
-
# Links between API Key Module and Other Modules
This document showcases the module links defined between the API Key Module and other Commerce Modules.
@@ -21550,1183 +21639,6 @@ createRemoteLinkStep({
```
-# Auth Module Provider
-
-In this guide, you’ll learn about the Auth Module Provider and how it's used.
-
-## What is an Auth Module Provider?
-
-An Auth Module Provider handles authenticating customers and users, either using custom logic or by integrating a third-party service.
-
-For example, the EmailPass Auth Module Provider authenticates a user using their email and password, whereas the Google Auth Module Provider authenticates users using their Google account.
-
-### Auth Providers List
-
-- [Emailpass](https://docs.medusajs.com/commerce-modules/auth/auth-providers/emailpass/index.html.md)
-- [Google](https://docs.medusajs.com/commerce-modules/auth/auth-providers/google/index.html.md)
-- [GitHub](https://docs.medusajs.com/commerce-modules/auth/auth-providers/github/index.html.md)
-
-***
-
-## How to Create an Auth Module Provider?
-
-An Auth Module Provider is a module whose service extends the `AbstractAuthModuleProvider` imported from `@medusajs/framework/utils`.
-
-The module can have multiple auth provider services, where each is registered as a separate auth provider.
-
-Refer to the [Create Auth Module Provider](https://docs.medusajs.com/references/auth/provider/index.html.md) guide to learn how to create an Auth Module Provider.
-
-***
-
-## Configure Allowed Auth Providers of Actor Types
-
-By default, users of all actor types can authenticate with all installed Auth Module Providers.
-
-To restrict the auth providers used for actor types, use the [authMethodsPerActor option](https://docs.medusajs.com/docs/learn/configurations/medusa-config#httpauthMethodsPerActor/index.html.md) in Medusa's configurations:
-
-```ts title="medusa-config.ts"
-module.exports = defineConfig({
- projectConfig: {
- http: {
- authMethodsPerActor: {
- user: ["google"],
- customer: ["emailpass"],
- },
- // ...
- },
- // ...
- },
-})
-```
-
-When you specify the `authMethodsPerActor` configuration, it overrides the default. So, if you don't specify any providers for an actor type, users of that actor type can't authenticate with any provider.
-
-
-# How to Use Authentication Routes
-
-In this document, you'll learn about the authentication routes and how to use them to create and log-in users, and reset their password.
-
-These routes are added by Medusa's HTTP layer, not the Auth Module.
-
-## Types of Authentication Flows
-
-### 1. Basic Authentication Flow
-
-This authentication flow doesn't require validation with third-party services.
-
-[How to register customer in storefront using basic authentication flow](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/register/index.html.md).
-
-The steps are:
-
-
-
-1. Register the user with the [Register Route](#register-route).
-2. Use the authentication token to create the user with their respective API route.
- - For example, for customers you would use the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers).
- - For admin users, you accept an invite using the [Accept Invite API route](https://docs.medusajs.com/api/admin#invites_postinvitesaccept)
-3. Authenticate the user with the [Auth Route](#login-route).
-
-After registration, you only use the [Auth Route](#login-route) for subsequent authentication.
-
-To handle errors related to existing identities, refer to [this section](#handling-existing-identities).
-
-### 2. Third-Party Service Authenticate Flow
-
-This authentication flow authenticates the user with a third-party service, such as Google.
-
-[How to authenticate customer with a third-party provider in the storefront.](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/third-party-login/index.html.md).
-
-It requires the following steps:
-
-
-
-1. Authenticate the user with the [Auth Route](#login-route).
-2. The auth route returns a URL to authenticate with third-party service, such as login with Google. The frontend (such as a storefront), when it receives a `location` property in the response, must redirect to the returned location.
-3. Once the authentication with the third-party service finishes, it redirects back to the frontend with a `code` query parameter. So, make sure your third-party service is configured to redirect to your frontend page after successful authentication.
-4. The frontend sends a request to the [Validate Callback Route](#validate-callback-route) passing it the query parameters received from the third-party service, such as the `code` and `state` query parameters.
-5. If the callback validation is successful, the frontend receives the authentication token.
-6. Decode the received token in the frontend using tools like [react-jwt](https://www.npmjs.com/package/react-jwt).
- - If the decoded data has an `actor_id` property, then the user is already registered. So, use this token for subsequent authenticated requests.
- - If not, follow the rest of the steps.
-7. The frontend uses the authentication token to create the user with their respective API route.
- - For example, for customers you would use the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers).
- - For admin users, you accept an invite using the [Accept Invite API route](https://docs.medusajs.com/api/admin#invites_postinvitesaccept)
-8. The frontend sends a request to the [Refresh Token Route](#refresh-token-route) to retrieve a new token with the user information populated.
-
-***
-
-## Register Route
-
-The Medusa application defines an API route at `/auth/{actor_type}/{provider}/register` that creates an auth identity for an actor type, such as a `customer`. It returns a JWT token that you pass to an API route that creates the user.
-
-```bash
-curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/register
--H 'Content-Type: application/json' \
---data-raw '{
- "email": "Whitney_Schultz@gmail.com"
- // ...
-}'
-```
-
-This API route is useful for providers like `emailpass` that uses custom logic to authenticate a user. For authentication providers that authenticate with third-party services, such as Google, use the [Auth Route](#login-route) instead.
-
-For example, if you're registering a customer, you:
-
-1. Send a request to `/auth/customer/emailpass/register` to retrieve the registration JWT token.
-2. Send a request to the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers) to create the customer, passing the [JWT token in the header](https://docs.medusajs.com/api/store#authentication).
-
-### Path Parameters
-
-Its path parameters are:
-
-- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`.
-- `{provider}`: the auth provider to handle the authentication. For example, `emailpass`.
-
-### Request Body Parameters
-
-This route accepts in the request body the data that the specified authentication provider requires to handle authentication.
-
-For example, the EmailPass provider requires an `email` and `password` fields in the request body.
-
-### Response Fields
-
-If the authentication is successful, you'll receive a `token` field in the response body object:
-
-```json
-{
- "token": "..."
-}
-```
-
-Use that token in the header of subsequent requests to send authenticated requests.
-
-### Handling Existing Identities
-
-An auth identity with the same email may already exist in Medusa. This can happen if:
-
-- Another actor type is using that email. For example, an admin user is trying to register as a customer.
-- The same email belongs to a record of the same actor type. For example, another customer has the same email.
-
-In these scenarios, the Register Route will return an error instead of a token:
-
-```json
-{
- "type": "unauthorized",
- "message": "Identity with email already exists"
-}
-```
-
-To handle these scenarios, you can use the [Login Route](#login-route) to validate that the email and password match the existing identity. If so, you can allow the admin user, for example, to register as a customer.
-
-Otherwise, if the email and password don't match the existing identity, such as when the email belongs to another customer, the [Login Route](#login-route) returns an error:
-
-```json
-{
- "type": "unauthorized",
- "message": "Invalid email or password"
-}
-```
-
-You can show that error message to the customer.
-
-***
-
-## Login Route
-
-The Medusa application defines an API route at `/auth/{actor_type}/{provider}` that authenticates a user of an actor type. It returns a JWT token that can be passed in [the header of subsequent requests](https://docs.medusajs.com/api/store#authentication) to send authenticated requests.
-
-```bash
-curl -X POST http://localhost:9000/auth/{actor_type}/{providers}
--H 'Content-Type: application/json' \
---data-raw '{
- "email": "Whitney_Schultz@gmail.com"
- // ...
-}'
-```
-
-For example, if you're authenticating a customer, you send a request to `/auth/customer/emailpass`.
-
-### Path Parameters
-
-Its path parameters are:
-
-- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`.
-- `{provider}`: the auth provider to handle the authentication. For example, `emailpass`.
-
-### Request Body Parameters
-
-This route accepts in the request body the data that the specified authentication provider requires to handle authentication.
-
-For example, the EmailPass provider requires an `email` and `password` fields in the request body.
-
-#### Overriding Callback URL
-
-For the [GitHub](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/github/index.html.md) and [Google](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/google/index.html.md) providers, you can pass a `callback_url` body parameter that overrides the `callbackUrl` set in the provider's configurations.
-
-This is useful if you want to redirect the user to a different URL after authentication based on their actor type. For example, you can set different `callback_url` for admin users and customers.
-
-### Response Fields
-
-If the authentication is successful, you'll receive a `token` field in the response body object:
-
-```json
-{
- "token": "..."
-}
-```
-
-Use that token in the header of subsequent requests to send authenticated requests.
-
-If the authentication requires more action with a third-party service, you'll receive a `location` property:
-
-```json
-{
- "location": "https://..."
-}
-```
-
-Redirect to that URL in the frontend to continue the authentication process with the third-party service.
-
-[How to login Customers using the authentication route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/login/index.html.md).
-
-***
-
-## Validate Callback Route
-
-The Medusa application defines an API route at `/auth/{actor_type}/{provider}/callback` that's useful for validating the authentication callback or redirect from third-party services like Google.
-
-```bash
-curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/callback?code=123&state=456
-```
-
-Refer to the [third-party authentication flow](#2-third-party-service-authenticate-flow) section to see how this route fits into the authentication flow.
-
-### Path Parameters
-
-Its path parameters are:
-
-- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`.
-- `{provider}`: the auth provider to handle the authentication. For example, `google`.
-
-### Query Parameters
-
-This route accepts all the query parameters that the third-party service sends to the frontend after the user completes the authentication process, such as the `code` and `state` query parameters.
-
-### Response Fields
-
-If the authentication is successful, you'll receive a `token` field in the response body object:
-
-```json
-{
- "token": "..."
-}
-```
-
-In your frontend, decode the token using tools like [react-jwt](https://www.npmjs.com/package/react-jwt):
-
-- If the decoded data has an `actor_id` property, the user is already registered. So, use this token for subsequent authenticated requests.
-- If not, use the token in the header of a request that creates the user, such as the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers).
-
-***
-
-## Refresh Token Route
-
-The Medusa application defines an API route at `/auth/token/refresh` that's useful after authenticating a user with a third-party service to populate the user's token with their new information.
-
-It requires the user's JWT token that they received from the authentication or callback routes.
-
-```bash
-curl -X POST http://localhost:9000/auth/token/refresh \
--H 'Authorization: Bearer {token}'
-```
-
-### Response Fields
-
-If the token was refreshed successfully, you'll receive a `token` field in the response body object:
-
-```json
-{
- "token": "..."
-}
-```
-
-Use that token in the header of subsequent requests to send authenticated requests.
-
-***
-
-## Reset Password Routes
-
-To reset a user's password:
-
-1. Generate a token using the [Generate Reset Password Token API route](#generate-reset-password-token-route).
- - The API route emits the `auth.password_reset` event, passing the token in the payload.
- - You can create a subscriber, as seen in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/reset-password/index.html.md), that listens to the event and send a notification to the user.
-2. Pass the token to the [Reset Password API route](#reset-password-route) to reset the password.
- - The URL in the user's notification should direct them to a frontend URL, which sends a request to this route.
-
-[Storefront Development: How to Reset a Customer's Password.](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/reset-password/index.html.md)
-
-### Generate Reset Password Token Route
-
-The Medusa application defines an API route at `/auth/{actor_type}/{auth_provider}/reset-password` that emits the `auth.password_reset` event, passing the token in the payload.
-
-```bash
-curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/reset-password
--H 'Content-Type: application/json' \
---data-raw '{
- "identifier": "Whitney_Schultz@gmail.com"
-}'
-```
-
-This API route is useful for providers like `emailpass` that store a user's password and use it for authentication.
-
-#### Path Parameters
-
-Its path parameters are:
-
-- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`.
-- `{provider}`: the auth provider to handle the authentication. For example, `emailpass`.
-
-#### Request Body Parameters
-
-This route accepts in the request body an object having the following property:
-
-- `identifier`: The user's identifier in the specified auth provider. For example, for the `emailpass` auth provider, you pass the user's email.
-
-#### Response Fields
-
-If the authentication is successful, the request returns a `201` response code.
-
-### Reset Password Route
-
-The Medusa application defines an API route at `/auth/{actor_type}/{auth_provider}/update` that accepts a token and, if valid, updates the user's password.
-
-```bash
-curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/update
--H 'Content-Type: application/json' \
--H 'Authorization: Bearer {token}' \
---data-raw '{
- "email": "Whitney_Schultz@gmail.com",
- "password": "supersecret"
-}'
-```
-
-This API route is useful for providers like `emailpass` that store a user's password and use it for logging them in.
-
-#### Path Parameters
-
-Its path parameters are:
-
-- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`.
-- `{provider}`: the auth provider to handle the authentication. For example, `emailpass`.
-
-#### Pass Token in Authorization Header
-
-Before [Medusa v2.6](https://github.com/medusajs/medusa/releases/tag/v2.6), you passed the token as a query parameter. Now, you must pass it in the `Authorization` header.
-
-In the request's authorization header, you must pass the token generated using the [Generate Reset Password Token route](#generate-reset-password-token-route). You pass it as a bearer token.
-
-### Request Body Parameters
-
-This route accepts in the request body an object that has the data necessary for the provider to update the user's password.
-
-For the `emailpass` provider, you must pass the following properties:
-
-- `email`: The user's email.
-- `password`: The new password.
-
-### Response Fields
-
-If the authentication is successful, the request returns an object with a `success` property set to `true`:
-
-```json
-{
- "success": "true"
-}
-```
-
-
-# Authentication Flows with the Auth Main Service
-
-In this document, you'll learn how to use the Auth Module's main service's methods to implement authentication flows and reset a user's password.
-
-## Authentication Methods
-
-### Register
-
-The [register method of the Auth Module's main service](https://docs.medusajs.com/references/auth/register/index.html.md) creates an auth identity that can be authenticated later.
-
-For example:
-
-```ts
-const data = await authModuleService.register(
- "emailpass",
- // passed to auth provider
- {
- // ...
- }
-)
-```
-
-This method calls the `register` method of the provider specified in the first parameter and returns its data.
-
-### Authenticate
-
-To authenticate a user, you use the [authenticate method of the Auth Module's main service](https://docs.medusajs.com/references/auth/authenticate/index.html.md). For example:
-
-```ts
-const data = await authModuleService.authenticate(
- "emailpass",
- // passed to auth provider
- {
- // ...
- }
-)
-```
-
-This method calls the `authenticate` method of the provider specified in the first parameter and returns its data.
-
-***
-
-## Auth Flow 1: Basic Authentication
-
-The basic authentication flow requires first using the `register` method, then the `authenticate` method:
-
-```ts
-const { success, authIdentity, error } = await authModuleService.register(
- "emailpass",
- // passed to auth provider
- {
- // ...
- }
-)
-
-if (error) {
- // registration failed
- // TODO return an error
- return
-}
-
-// later (can be another route for log-in)
-const { success, authIdentity, location } = await authModuleService.authenticate(
- "emailpass",
- // passed to auth provider
- {
- // ...
- }
-)
-
-if (success && !location) {
- // user is authenticated
-}
-```
-
-If `success` is true and `location` isn't set, the user is authenticated successfully, and their authentication details are available within the `authIdentity` object.
-
-The next section explains the flow if `location` is set.
-
-Check out the [AuthIdentity](https://docs.medusajs.com/references/auth/models/AuthIdentity/index.html.md) reference for the received properties in `authIdentity`.
-
-
-
-### Auth Identity with Same Identifier
-
-If an auth identity, such as a `customer`, tries to register with an email of another auth identity, the `register` method returns an error. This can happen either if another customer is using the same email, or an admin user has the same email.
-
-There are two ways to handle this:
-
-- Consider the customer authenticated if the `authenticate` method validates that the email and password are correct. This allows admin users, for example, to authenticate as customers.
-- Return an error message to the customer, informing them that the email is already in use.
-
-***
-
-## Auth Flow 2: Third-Party Service Authentication
-
-The third-party service authentication method requires using the `authenticate` method first:
-
-```ts
-const { success, authIdentity, location } = await authModuleService.authenticate(
- "google",
- // passed to auth provider
- {
- // ...
- }
-)
-
-if (location) {
- // return the location for the front-end to redirect to
-}
-
-if (!success) {
- // authentication failed
-}
-
-// authentication successful
-```
-
-If the `authenticate` method returns a `location` property, the authentication process requires the user to perform an action with a third-party service. So, you return the `location` to the front-end or client to redirect to that URL.
-
-For example, when using the `google` provider, the `location` is the URL that the user is navigated to login.
-
-
-
-### Overriding Callback URL
-
-The Google and GitHub providers allow you to override their `callbackUrl` option during authentication. This is useful when you redirect the user after authentication to a URL based on its actor type. For example, you redirect admin users and customers to different pages.
-
-```ts
-const { success, authIdentity, location } = await authModuleService.authenticate(
- "google",
- // passed to auth provider
- {
- // ...
- callback_url: "example.com",
- }
-)
-```
-
-### validateCallback
-
-Providers handling this authentication flow must implement the `validateCallback` method. It implements the logic to validate the authentication with the third-party service.
-
-So, once the user performs the required action with the third-party service (for example, log-in with Google), the frontend must redirect to an API route that uses the [validateCallback method of the Auth Module's main service](https://docs.medusajs.com/references/auth/validateCallback/index.html.md).
-
-The method calls the specified provider’s `validateCallback` method passing it the authentication details it received in the second parameter:
-
-```ts
-const { success, authIdentity } = await authModuleService.validateCallback(
- "google",
- // passed to auth provider
- {
- // request data, such as
- url,
- headers,
- query,
- body,
- protocol,
- }
-)
-
-if (success) {
- // authentication succeeded
-}
-```
-
-For providers like Google, the `query` object contains the query parameters from the original callback URL, such as the `code` and `state` parameters.
-
-If the returned `success` property is `true`, the authentication with the third-party provider was successful.
-
-
-
-***
-
-## Reset Password
-
-To update a user's password or other authentication details, use the `updateProvider` method of the Auth Module's main service. It calls the `update` method of the specified authentication provider.
-
-For example:
-
-```ts
-const { success } = await authModuleService.updateProvider(
- "emailpass",
- // passed to the auth provider
- {
- entity_id: "user@example.com",
- password: "supersecret",
- }
-)
-
-if (success) {
- // password reset successfully
-}
-```
-
-The method accepts as a first parameter the ID of the provider, and as a second parameter the data necessary to reset the password.
-
-In the example above, you use the `emailpass` provider, so you have to pass an object having an `email` and `password` properties.
-
-If the returned `success` property is `true`, the password has reset successfully.
-
-
-# Auth Module Options
-
-In this document, you'll learn about the options of the Auth Module.
-
-## providers
-
-The `providers` option is an array of auth module providers.
-
-When the Medusa application starts, these providers are registered and can be used to handle authentication.
-
-By default, the `emailpass` provider is registered to authenticate customers and admin users.
-
-For example:
-
-```ts title="medusa-config.ts"
-import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils"
-
-// ...
-
-module.exports = defineConfig({
- // ...
- modules: [
- {
- resolve: "@medusajs/medusa/auth",
- dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER],
- options: {
- providers: [
- {
- resolve: "@medusajs/medusa/auth-emailpass",
- id: "emailpass",
- options: {
- // provider options...
- },
- },
- ],
- },
- },
- ],
-})
-```
-
-The `providers` option is an array of objects that accept the following properties:
-
-- `resolve`: A string indicating the package name of the module provider or the path to it relative to the `src` directory.
-- `id`: A string indicating the provider's unique name or ID.
-- `options`: An optional object of the module provider's options.
-
-***
-
-## Auth CORS
-
-The Medusa application's authentication API routes are defined under the `/auth` prefix that requires setting the `authCors` property of the `http` configuration.
-
-By default, the Medusa application you created will have an `AUTH_CORS` environment variable, which is used as the value of `authCors`.
-
-Refer to [Medusa's configuration guide](https://docs.medusajs.com/docs/learn/configurations/medusa-config#httpauthCors/index.html.md) to learn more about the `authCors` configuration.
-
-***
-
-## authMethodsPerActor Configuration
-
-The Medusa application's configuration accept an `authMethodsPerActor` configuration which restricts the allowed auth providers used with an actor type.
-
-Learn more about the `authMethodsPerActor` configuration in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers#configure-allowed-auth-providers-of-actor-types/index.html.md).
-
-
-# How to Create an Actor Type
-
-In this document, learn how to create an actor type and authenticate its associated data model.
-
-## 0. Create Module with Data Model
-
-Before creating an actor type, you must have a module with a data model representing the actor type.
-
-Learn how to create a module in [this guide](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md).
-
-The rest of this guide uses this `Manager` data model as an example:
-
-```ts title="src/modules/manager/models/manager.ts"
-import { model } from "@medusajs/framework/utils"
-
-const Manager = model.define("manager", {
- id: model.id().primaryKey(),
- firstName: model.text(),
- lastName: model.text(),
- email: model.text(),
-})
-
-export default Manager
-```
-
-***
-
-## 1. Create Workflow
-
-Start by creating a workflow that does two things:
-
-- Creates a record of the `Manager` data model.
-- Sets the `app_metadata` property of the associated `AuthIdentity` record based on the new actor type.
-
-For example, create the file `src/workflows/create-manager.ts`. with the following content:
-
-```ts title="src/workflows/create-manager.ts" highlights={workflowHighlights}
-import {
- createWorkflow,
- createStep,
- StepResponse,
- WorkflowResponse,
-} from "@medusajs/framework/workflows-sdk"
-import {
- setAuthAppMetadataStep,
-} from "@medusajs/medusa/core-flows"
-import ManagerModuleService from "../modules/manager/service"
-
-type CreateManagerWorkflowInput = {
- manager: {
- first_name: string
- last_name: string
- email: string
- }
- authIdentityId: string
-}
-
-const createManagerStep = createStep(
- "create-manager-step",
- async ({
- manager: managerData,
- }: Pick,
- { container }) => {
- const managerModuleService: ManagerModuleService =
- container.resolve("manager")
-
- const manager = await managerModuleService.createManager(
- managerData
- )
-
- return new StepResponse(manager)
- }
-)
-
-const createManagerWorkflow = createWorkflow(
- "create-manager",
- function (input: CreateManagerWorkflowInput) {
- const manager = createManagerStep({
- manager: input.manager,
- })
-
- setAuthAppMetadataStep({
- authIdentityId: input.authIdentityId,
- actorType: "manager",
- value: manager.id,
- })
-
- return new WorkflowResponse(manager)
- }
-)
-
-export default createManagerWorkflow
-```
-
-This workflow accepts the manager’s data and the associated auth identity’s ID as inputs. The next sections explain how the auth identity ID is retrieved.
-
-The workflow has two steps:
-
-1. Create the manager using the `createManagerStep`.
-2. Set the `app_metadata` property of the associated auth identity using the `setAuthAppMetadataStep` from Medusa's core workflows. You specify the actor type `manager` in the `actorType` property of the step’s input.
-
-***
-
-## 2. Define the Create API Route
-
-Next, you’ll use the workflow defined in the previous section in an API route that creates a manager.
-
-So, create the file `src/api/manager/route.ts` with the following content:
-
-```ts title="src/api/manager/route.ts" highlights={createRouteHighlights}
-import type {
- AuthenticatedMedusaRequest,
- MedusaResponse,
-} from "@medusajs/framework/http"
-import { MedusaError } from "@medusajs/framework/utils"
-import createManagerWorkflow from "../../workflows/create-manager"
-
-type RequestBody = {
- first_name: string
- last_name: string
- email: string
-}
-
-export async function POST(
- req: AuthenticatedMedusaRequest,
- res: MedusaResponse
-) {
- // If `actor_id` is present, the request carries
- // authentication for an existing manager
- if (req.auth_context.actor_id) {
- throw new MedusaError(
- MedusaError.Types.INVALID_DATA,
- "Request already authenticated as a manager."
- )
- }
-
- const { result } = await createManagerWorkflow(req.scope)
- .run({
- input: {
- manager: req.body,
- authIdentityId: req.auth_context.auth_identity_id,
- },
- })
-
- res.status(200).json({ manager: result })
-}
-```
-
-Since the manager must be associated with an `AuthIdentity` record, the request is expected to be authenticated, even if the manager isn’t created yet. This can be achieved by:
-
-1. Obtaining a token usng the [/auth route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route/index.html.md).
-2. Passing the token in the bearer header of the request to this route.
-
-In the API route, you create the manager using the workflow from the previous section and return it in the response.
-
-***
-
-## 3. Apply the `authenticate` Middleware
-
-The last step is to apply the `authenticate` middleware on the API routes that require a manager’s authentication.
-
-To do that, create the file `src/api/middlewares.ts` with the following content:
-
-```ts title="src/api/middlewares.ts" highlights={middlewareHighlights}
-import {
- defineMiddlewares,
- authenticate,
-} from "@medusajs/framework/http"
-
-export default defineMiddlewares({
- routes: [
- {
- matcher: "/manager",
- method: "POST",
- middlewares: [
- authenticate("manager", ["session", "bearer"], {
- allowUnregistered: true,
- }),
- ],
- },
- {
- matcher: "/manager/me*",
- middlewares: [
- authenticate("manager", ["session", "bearer"]),
- ],
- },
- ],
-})
-```
-
-This applies middlewares on two route patterns:
-
-1. The `authenticate` middleware is applied on the `/manager` API route for `POST` requests while allowing unregistered managers. This requires that a bearer token be passed in the request to access the manager’s auth identity but doesn’t require the manager to be registered.
-2. The `authenticate` middleware is applied on all routes starting with `/manager/me`, restricting these routes to authenticated managers only.
-
-### Retrieve Manager API Route
-
-For example, create the file `src/api/manager/me/route.ts` with the following content:
-
-```ts title="src/api/manager/me/route.ts"
-import {
- AuthenticatedMedusaRequest,
- MedusaResponse,
-} from "@medusajs/framework/http"
-import ManagerModuleService from "../../../modules/manager/service"
-
-export async function GET(
- req: AuthenticatedMedusaRequest,
- res: MedusaResponse
-): Promise {
- const query = req.scope.resolve("query")
- const managerId = req.auth_context?.actor_id
-
- const { data: [manager] } = await query.graph({
- entity: "manager",
- fields: ["*"],
- filters: {
- id: managerId,
- },
- }, {
- throwIfKeyNotFound: true,
- })
-
- res.json({ manager })
-}
-```
-
-This route is only accessible by authenticated managers. You access the manager’s ID using `req.auth_context.actor_id`.
-
-***
-
-## Test Custom Actor Type Authentication Flow
-
-To authenticate managers:
-
-1. Send a `POST` request to `/auth/manager/emailpass/register` to create an auth identity for the manager:
-
-```bash
-curl -X POST 'http://localhost:9000/auth/manager/emailpass/register' \
--H 'Content-Type: application/json' \
---data-raw '{
- "email": "manager@gmail.com",
- "password": "supersecret"
-}'
-```
-
-Copy the returned token to use it in the next request.
-
-2. Send a `POST` request to `/manager` to create a manager:
-
-```bash
-curl -X POST 'http://localhost:9000/manager' \
--H 'Content-Type: application/json' \
--H 'Authorization: Bearer {token}' \
---data-raw '{
- "first_name": "John",
- "last_name": "Doe",
- "email": "manager@gmail.com"
-}'
-```
-
-Replace `{token}` with the token returned in the previous step.
-
-3. Send a `POST` request to `/auth/manager/emailpass` again to retrieve an authenticated token for the manager:
-
-```bash
-curl -X POST 'http://localhost:9000/auth/manager/emailpass' \
--H 'Content-Type: application/json' \
---data-raw '{
- "email": "manager@gmail.com",
- "password": "supersecret"
-}'
-```
-
-4. You can now send authenticated requests as a manager. For example, send a `GET` request to `/manager/me` to retrieve the authenticated manager’s details:
-
-```bash
-curl 'http://localhost:9000/manager/me' \
--H 'Authorization: Bearer {token}'
-```
-
-Whenever you want to log in as a manager, use the `/auth/manager/emailpass` API route, as explained in step 3.
-
-***
-
-## Delete User of Actor Type
-
-When you delete a user of the actor type, you must update its auth identity to remove the association to the user.
-
-For example, create the following workflow that deletes a manager and updates its auth identity, create the file `src/workflows/delete-manager.ts` with the following content:
-
-```ts title="src/workflows/delete-manager.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports"
-import {
- createStep,
- StepResponse,
-} from "@medusajs/framework/workflows-sdk"
-import ManagerModuleService from "../modules/manager/service"
-
-export type DeleteManagerWorkflow = {
- id: string
-}
-
-const deleteManagerStep = createStep(
- "delete-manager-step",
- async (
- { id }: DeleteManagerWorkflow,
- { container }) => {
- const managerModuleService: ManagerModuleService =
- container.resolve("manager")
-
- const manager = await managerModuleService.retrieve(id)
-
- await managerModuleService.deleteManagers(id)
-
- return new StepResponse(undefined, { manager })
- },
- async ({ manager }, { container }) => {
- const managerModuleService: ManagerModuleService =
- container.resolve("manager")
-
- await managerModuleService.createManagers(manager)
- }
- )
-```
-
-You add a step that deletes the manager using the `deleteManagers` method of the module's main service. In the compensation function, you create the manager again.
-
-Next, in the same file, add the workflow that deletes a manager:
-
-```ts title="src/workflows/delete-manager.ts" collapsibleLines="1-15" expandButtonLabel="Show Imports" highlights={deleteHighlights}
-// other imports
-import { MedusaError } from "@medusajs/framework/utils"
-import {
- WorkflowData,
- WorkflowResponse,
- createWorkflow,
- transform,
-} from "@medusajs/framework/workflows-sdk"
-import {
- setAuthAppMetadataStep,
- useQueryGraphStep,
-} from "@medusajs/medusa/core-flows"
-
-// ...
-
-export const deleteManagerWorkflow = createWorkflow(
- "delete-manager",
- (
- input: WorkflowData
- ): WorkflowResponse => {
- deleteManagerStep(input)
-
- const { data: authIdentities } = useQueryGraphStep({
- entity: "auth_identity",
- fields: ["id"],
- filters: {
- app_metadata: {
- // the ID is of the format `{actor_type}_id`.
- manager_id: input.id,
- },
- },
- })
-
- const authIdentity = transform(
- { authIdentities },
- ({ authIdentities }) => {
- const authIdentity = authIdentities[0]
-
- if (!authIdentity) {
- throw new MedusaError(
- MedusaError.Types.NOT_FOUND,
- "Auth identity not found"
- )
- }
-
- return authIdentity
- }
- )
-
- setAuthAppMetadataStep({
- authIdentityId: authIdentity.id,
- actorType: "manager",
- value: null,
- })
-
- return new WorkflowResponse(input.id)
- }
-)
-```
-
-In the workflow, you:
-
-1. Use the `deleteManagerStep` defined earlier to delete the manager.
-2. Retrieve the auth identity of the manager using Query. To do that, you filter the `app_metadata` property of an auth identity, which holds the user's ID under `{actor_type_name}_id`. So, in this case, it's `manager_id`.
-3. Check that the auth identity exist, then, update the auth identity to remove the ID of the manager from it.
-
-You can use this workflow when deleting a manager, such as in an API route.
-
-
-# How to Handle Password Reset Token Event
-
-In this guide, you'll learn how to handle the `auth.password_reset` event, which is emitted when a request is sent to the [Generate Reset Password Token API route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#generate-reset-password-token-route/index.html.md).
-
-Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/reset-password/index.html.md) to learn how to reset your user admin password using the dashboard.
-
-You'll create a subscriber that listens to the event. When the event is emitted, the subscriber sends an email notification to the user.
-
-### Prerequisites
-
-- [A notification provider module, such as SendGrid](https://docs.medusajs.com/infrastructure-modules/notification/sendgrid/index.html.md)
-
-## 1. Create Subscriber
-
-The first step is to create a subscriber that listens to the `auth.password_reset` and sends the user a notification with instructions to reset their password.
-
-Create the file `src/subscribers/handle-reset.ts` with the following content:
-
-```ts title="src/subscribers/handle-reset.ts" highlights={highlights} collapsibleLines="1-6" expandMoreLabel="Show Imports"
-import {
- SubscriberArgs,
- type SubscriberConfig,
-} from "@medusajs/medusa"
-import { Modules } from "@medusajs/framework/utils"
-
-export default async function resetPasswordTokenHandler({
- event: { data: {
- entity_id: email,
- token,
- actor_type,
- } },
- container,
-}: SubscriberArgs<{ entity_id: string, token: string, actor_type: string }>) {
- const notificationModuleService = container.resolve(
- Modules.NOTIFICATION
- )
-
- const urlPrefix = actor_type === "customer" ?
- "https://storefront.com" :
- "https://admin.com/app"
-
- await notificationModuleService.createNotifications({
- to: email,
- channel: "email",
- template: "reset-password-template",
- data: {
- // a URL to a frontend application
- url: `${urlPrefix}/reset-password?token=${token}&email=${email}`,
- },
- })
-}
-
-export const config: SubscriberConfig = {
- event: "auth.password_reset",
-}
-```
-
-You subscribe to the `auth.password_reset` event. The event has a data payload object with the following properties:
-
-- `entity_id`: The identifier of the user. When using the `emailpass` provider, it's the user's email.
-- `token`: The token to reset the user's password.
-- `actor_type`: The user's actor type. For example, if the user is a customer, the `actor_type` is `customer`. If it's an admin user, the `actor_type` is `user`.
-
-This event's payload previously had an `actorType` field. It was renamed to `actor_type` after [Medusa v2.0.7](https://github.com/medusajs/medusa/releases/tag/v2.0.7).
-
-In the subscriber, you:
-
-- Decide the frontend URL based on whether the user is a customer or admin user by checking the value of `actor_type`.
-- Resolve the Notification Module and use its `createNotifications` method to send the notification.
-- You pass to the `createNotifications` method an object having the following properties:
- - `to`: The identifier to send the notification to, which in this case is the email.
- - `channel`: The channel to send the notification through, which in this case is email.
- - `template`: The template ID in the third-party service.
- - `data`: The data payload to pass to the template. You pass the URL to redirect the user to. You must pass the token and email in the URL so that the frontend can send them later to the Medusa application when reseting the password.
-
-***
-
-## 2. Test it Out: Generate Reset Password Token
-
-To test the subscriber out, send a request to the `/auth/{actor_type}/{auth_provider}/reset-password` API route, replacing `{actor_type}` and `{auth_provider}` with the user's actor type and provider used for authentication respectively.
-
-For example, to generate a reset password token for an admin user using the `emailpass` provider, send the following request:
-
-```bash
-curl --location 'http://localhost:9000/auth/user/emailpass/reset-password' \
---header 'Content-Type: application/json' \
---data-raw '{
- "identifier": "admin-test@gmail.com"
-}'
-```
-
-In the request body, you must pass an `identifier` parameter. Its value is the user's identifier, which is the email in this case.
-
-If the token is generated successfully, the request returns a response with `201` status code. In the terminal, you'll find the following message indicating that the `auth.password_reset` event was emitted and your subscriber ran:
-
-```plain
-info: Processing auth.password_reset which has 1 subscribers
-```
-
-The notification is sent to the user with the frontend URL to enter a new password.
-
-***
-
-## Next Steps: Implementing Frontend
-
-In your frontend, you must have a page that accepts `token` and `email` query parameters.
-
-The page shows the user password fields to enter their new password, then submits the new password, token, and email to the [Reset Password Route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#reset-password-route/index.html.md).
-
-### Examples
-
-- [Storefront Guide: Reset Customer Password](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/reset-password/index.html.md)
-
-
# Cart Concepts
In this document, you’ll get an overview of the main concepts of a cart.
@@ -23396,839 +22308,1250 @@ await cartModuleService.setLineItemTaxLines(
```
-# Customer Accounts
+# Authentication Flows with the Auth Main Service
-In this document, you’ll learn how registered and unregistered accounts are distinguished in the Medusa application.
+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.
-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.
+## Authentication Methods
-## `has_account` Property
+### Register
-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.
+The [register method of the Auth Module's main service](https://docs.medusajs.com/references/auth/register/index.html.md) creates an auth identity that can be authenticated later.
-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
+For example:
```ts
-const { data: customers } = await query.graph({
- entity: "customer",
- fields: [
- "account_holder_link.account_holder.*",
- ],
-})
-
-// customers[0].account_holder_link?.[0]?.account_holder
-```
-
-### useQueryGraphStep
-
-```ts
-import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
-
-// ...
-
-const { data: customers } = useQueryGraphStep({
- entity: "customer",
- fields: [
- "account_holder_link.account_holder.*",
- ],
-})
-
-// customers[0].account_holder_link?.[0]?.account_holder
-```
-
-### Manage with Link
-
-To manage the account holders of a customer, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):
-
-### link.create
-
-```ts
-import { Modules } from "@medusajs/framework/utils"
-
-// ...
-
-await link.create({
- [Modules.CUSTOMER]: {
- customer_id: "cus_123",
- },
- [Modules.PAYMENT]: {
- account_holder_id: "acchld_123",
- },
-})
-```
-
-### createRemoteLinkStep
-
-```ts
-import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"
-
-// ...
-
-createRemoteLinkStep({
- [Modules.CUSTOMER]: {
- customer_id: "cus_123",
- },
- [Modules.PAYMENT]: {
- account_holder_id: "acchld_123",
- },
-})
-```
-
-***
-
-## Cart Module
-
-Medusa defines a read-only link between the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `Cart` data model and the `Customer` data model. Because the link is read-only from the `Cart`'s side, you can only retrieve the customer of a cart, and not the other way around.
-
-### Retrieve with Query
-
-To retrieve the customer of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`:
-
-### query.graph
-
-```ts
-const { data: carts } = await query.graph({
- entity: "cart",
- fields: [
- "customer.*",
- ],
-})
-
-// carts.customer
-```
-
-### useQueryGraphStep
-
-```ts
-import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
-
-// ...
-
-const { data: carts } = useQueryGraphStep({
- entity: "cart",
- fields: [
- "customer.*",
- ],
-})
-
-// carts.customer
-```
-
-***
-
-## Order Module
-
-Medusa defines a read-only link between the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `Order` data model and the `Customer` data model. Because the link is read-only from the `Order`'s side, you can only retrieve the customer of an order, and not the other way around.
-
-### Retrieve with Query
-
-To retrieve the customer of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`:
-
-### query.graph
-
-```ts
-const { data: orders } = await query.graph({
- entity: "order",
- fields: [
- "customer.*",
- ],
-})
-
-// orders.customer
-```
-
-### useQueryGraphStep
-
-```ts
-import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
-
-// ...
-
-const { data: orders } = useQueryGraphStep({
- entity: "order",
- fields: [
- "customer.*",
- ],
-})
-
-// orders.customer
-```
-
-
-# Inventory Concepts
-
-In this document, you’ll learn about the main concepts in the Inventory Module, and how data is stored and related.
-
-## 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 data = await authModuleService.register(
+ "emailpass",
+ // passed to auth provider
+ {
// ...
-
- 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.
+This method calls the `register` method of the provider specified in the first parameter and returns its data.
-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).
+### Authenticate
+
+To authenticate a user, you use the [authenticate method of the Auth Module's main service](https://docs.medusajs.com/references/auth/authenticate/index.html.md). For example:
+
+```ts
+const data = await authModuleService.authenticate(
+ "emailpass",
+ // passed to auth provider
+ {
+ // ...
+ }
+)
+```
+
+This method calls the `authenticate` method of the provider specified in the first parameter and returns its data.
***
-## Bundled Products
+## Auth Flow 1: Basic Authentication
-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.
+The basic authentication flow requires first using the `register` method, then the `authenticate` method:
-Consider you have three products: shirt, pants, and shoes. You sell those products separately, but you also want to offer them as a bundle.
+```ts
+const { success, authIdentity, error } = await authModuleService.register(
+ "emailpass",
+ // passed to auth provider
+ {
+ // ...
+ }
+)
-
+if (error) {
+ // registration failed
+ // TODO return an error
+ return
+}
-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.
+// later (can be another route for log-in)
+const { success, authIdentity, location } = await authModuleService.authenticate(
+ "emailpass",
+ // passed to auth provider
+ {
+ // ...
+ }
+)
-Then, when the bundled product's variant is purchased, the inventory quantity of the associated inventory items are updated.
+if (success && !location) {
+ // user is authenticated
+}
+```
-
+If `success` is true and `location` isn't set, the user is authenticated successfully, and their authentication details are available within the `authIdentity` object.
-### Create Bundled Product
+The next section explains the flow if `location` is set.
-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.
+Check out the [AuthIdentity](https://docs.medusajs.com/references/auth/models/AuthIdentity/index.html.md) reference for the received properties in `authIdentity`.
-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"
+### Auth Identity with Same Identifier
-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"],
- },
- ],
- },
- ],
- },
- })
+If an auth identity, such as a `customer`, tries to register with an email of another auth identity, the `register` method returns an error. This can happen either if another customer is using the same email, or an admin user has the same email.
- // TODO re-retrieve with inventory
+There are two ways to handle this:
+
+- Consider the customer authenticated if the `authenticate` method validates that the email and password are correct. This allows admin users, for example, to authenticate as customers.
+- Return an error message to the customer, informing them that the email is already in use.
+
+***
+
+## Auth Flow 2: Third-Party Service Authentication
+
+The third-party service authentication method requires using the `authenticate` method first:
+
+```ts
+const { success, authIdentity, location } = await authModuleService.authenticate(
+ "google",
+ // passed to auth provider
+ {
+ // ...
+ }
+)
+
+if (location) {
+ // return the location for the front-end to redirect to
+}
+
+if (!success) {
+ // authentication failed
+}
+
+// authentication successful
+```
+
+If the `authenticate` method returns a `location` property, the authentication process requires the user to perform an action with a third-party service. So, you return the `location` to the front-end or client to redirect to that URL.
+
+For example, when using the `google` provider, the `location` is the URL that the user is navigated to login.
+
+
+
+### Overriding Callback URL
+
+The Google and GitHub providers allow you to override their `callbackUrl` option during authentication. This is useful when you redirect the user after authentication to a URL based on its actor type. For example, you redirect admin users and customers to different pages.
+
+```ts
+const { success, authIdentity, location } = await authModuleService.authenticate(
+ "google",
+ // passed to auth provider
+ {
+ // ...
+ callback_url: "example.com",
}
)
```
-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).
+### validateCallback
-Next, retrieve the products again but with variant information:
+Providers handling this authentication flow must implement the `validateCallback` method. It implements the logic to validate the authentication with the third-party service.
-```ts highlights={bundledHighlights2}
+So, once the user performs the required action with the third-party service (for example, log-in with Google), the frontend must redirect to an API route that uses the [validateCallback method of the Auth Module's main service](https://docs.medusajs.com/references/auth/validateCallback/index.html.md).
+
+The method calls the specified provider’s `validateCallback` method passing it the authentication details it received in the second parameter:
+
+```ts
+const { success, authIdentity } = await authModuleService.validateCallback(
+ "google",
+ // passed to auth provider
+ {
+ // request data, such as
+ url,
+ headers,
+ query,
+ body,
+ protocol,
+ }
+)
+
+if (success) {
+ // authentication succeeded
+}
+```
+
+For providers like Google, the `query` object contains the query parameters from the original callback URL, such as the `code` and `state` parameters.
+
+If the returned `success` property is `true`, the authentication with the third-party provider was successful.
+
+
+
+***
+
+## Reset Password
+
+To update a user's password or other authentication details, use the `updateProvider` method of the Auth Module's main service. It calls the `update` method of the specified authentication provider.
+
+For example:
+
+```ts
+const { success } = await authModuleService.updateProvider(
+ "emailpass",
+ // passed to the auth provider
+ {
+ entity_id: "user@example.com",
+ password: "supersecret",
+ }
+)
+
+if (success) {
+ // password reset successfully
+}
+```
+
+The method accepts as a first parameter the ID of the provider, and as a second parameter the data necessary to reset the password.
+
+In the example above, you use the `emailpass` provider, so you have to pass an object having an `email` and `password` properties.
+
+If the returned `success` property is `true`, the password has reset successfully.
+
+
+# Auth Identity and Actor Types
+
+In this document, you’ll learn about concepts related to identity and actors in the Auth Module.
+
+## What is an Auth Identity?
+
+The [AuthIdentity data model](https://docs.medusajs.com/references/auth/models/AuthIdentity/index.html.md) represents a user registered by an [authentication provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/index.html.md). When a user is registered using an authentication provider, the provider creates a record of `AuthIdentity`.
+
+Then, when the user logs-in in the future with the same authentication provider, the associated auth identity is used to validate their credentials.
+
+***
+
+## Actor Types
+
+An actor type is a type of user that can be authenticated. The Auth Module doesn't store or manage any user-like models, such as for customers or users. Instead, the user types are created and managed by other modules. For example, a customer is managed by the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md).
+
+Then, when an auth identity is created for the actor type, the ID of the user is stored in the `app_metadata` property of the auth identity.
+
+For example, an auth identity of a customer has the following `app_metadata` property:
+
+```json
+{
+ "app_metadata": {
+ "customer_id": "cus_123"
+ }
+}
+```
+
+The ID of the user is stored in the key `{actor_type}_id` of the `app_metadata` property.
+
+***
+
+## Protect Routes by Actor Type
+
+When you protect routes with the `authenticate` middleware, you specify in its first parameter the actor type that must be authenticated to access the specified API routes.
+
+For example:
+
+```ts title="src/api/middlewares.ts" highlights={highlights}
import {
- // ...
- transform,
-} from "@medusajs/framework/workflows-sdk"
-import {
- useQueryGraphStep,
-} from "@medusajs/medusa/core-flows"
+ defineMiddlewares,
+ authenticate,
+} from "@medusajs/framework/http"
-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.*",
+export default defineMiddlewares({
+ routes: [
+ {
+ matcher: "/custom/admin*",
+ middlewares: [
+ authenticate("user", ["session", "bearer", "api-key"]),
],
+ },
+ ],
+})
+```
+
+By specifying `user` as the first parameter of `authenticate`, only authenticated users of actor type `user` (admin users) can access API routes starting with `/custom/admin`.
+
+***
+
+## Custom Actor Types
+
+You can define custom actor types that allows a custom user, managed by your custom module, to authenticate into Medusa.
+
+For example, if you have a custom module with a `Manager` data model, you can authenticate managers with the `manager` actor type.
+
+Learn how to create a custom actor type in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/create-actor-type/index.html.md).
+
+
+# Auth Module Provider
+
+In this guide, you’ll learn about the Auth Module Provider and how it's used.
+
+## What is an Auth Module Provider?
+
+An Auth Module Provider handles authenticating customers and users, either using custom logic or by integrating a third-party service.
+
+For example, the EmailPass Auth Module Provider authenticates a user using their email and password, whereas the Google Auth Module Provider authenticates users using their Google account.
+
+### Auth Providers List
+
+- [Emailpass](https://docs.medusajs.com/commerce-modules/auth/auth-providers/emailpass/index.html.md)
+- [Google](https://docs.medusajs.com/commerce-modules/auth/auth-providers/google/index.html.md)
+- [GitHub](https://docs.medusajs.com/commerce-modules/auth/auth-providers/github/index.html.md)
+
+***
+
+## How to Create an Auth Module Provider?
+
+An Auth Module Provider is a module whose service extends the `AbstractAuthModuleProvider` imported from `@medusajs/framework/utils`.
+
+The module can have multiple auth provider services, where each is registered as a separate auth provider.
+
+Refer to the [Create Auth Module Provider](https://docs.medusajs.com/references/auth/provider/index.html.md) guide to learn how to create an Auth Module Provider.
+
+***
+
+## Configure Allowed Auth Providers of Actor Types
+
+By default, users of all actor types can authenticate with all installed Auth Module Providers.
+
+To restrict the auth providers used for actor types, use the [authMethodsPerActor option](https://docs.medusajs.com/docs/learn/configurations/medusa-config#httpauthMethodsPerActor/index.html.md) in Medusa's configurations:
+
+```ts title="medusa-config.ts"
+module.exports = defineConfig({
+ projectConfig: {
+ http: {
+ authMethodsPerActor: {
+ user: ["google"],
+ customer: ["emailpass"],
+ },
+ // ...
+ },
+ // ...
+ },
+})
+```
+
+When you specify the `authMethodsPerActor` configuration, it overrides the default. So, if you don't specify any providers for an actor type, users of that actor type can't authenticate with any provider.
+
+
+# How to Use Authentication Routes
+
+In this document, you'll learn about the authentication routes and how to use them to create and log-in users, and reset their password.
+
+These routes are added by Medusa's HTTP layer, not the Auth Module.
+
+## Types of Authentication Flows
+
+### 1. Basic Authentication Flow
+
+This authentication flow doesn't require validation with third-party services.
+
+[How to register customer in storefront using basic authentication flow](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/register/index.html.md).
+
+The steps are:
+
+
+
+1. Register the user with the [Register Route](#register-route).
+2. Use the authentication token to create the user with their respective API route.
+ - For example, for customers you would use the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers).
+ - For admin users, you accept an invite using the [Accept Invite API route](https://docs.medusajs.com/api/admin#invites_postinvitesaccept)
+3. Authenticate the user with the [Auth Route](#login-route).
+
+After registration, you only use the [Auth Route](#login-route) for subsequent authentication.
+
+To handle errors related to existing identities, refer to [this section](#handling-existing-identities).
+
+### 2. Third-Party Service Authenticate Flow
+
+This authentication flow authenticates the user with a third-party service, such as Google.
+
+[How to authenticate customer with a third-party provider in the storefront.](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/third-party-login/index.html.md).
+
+It requires the following steps:
+
+
+
+1. Authenticate the user with the [Auth Route](#login-route).
+2. The auth route returns a URL to authenticate with third-party service, such as login with Google. The frontend (such as a storefront), when it receives a `location` property in the response, must redirect to the returned location.
+3. Once the authentication with the third-party service finishes, it redirects back to the frontend with a `code` query parameter. So, make sure your third-party service is configured to redirect to your frontend page after successful authentication.
+4. The frontend sends a request to the [Validate Callback Route](#validate-callback-route) passing it the query parameters received from the third-party service, such as the `code` and `state` query parameters.
+5. If the callback validation is successful, the frontend receives the authentication token.
+6. Decode the received token in the frontend using tools like [react-jwt](https://www.npmjs.com/package/react-jwt).
+ - If the decoded data has an `actor_id` property, then the user is already registered. So, use this token for subsequent authenticated requests.
+ - If not, follow the rest of the steps.
+7. The frontend uses the authentication token to create the user with their respective API route.
+ - For example, for customers you would use the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers).
+ - For admin users, you accept an invite using the [Accept Invite API route](https://docs.medusajs.com/api/admin#invites_postinvitesaccept)
+8. The frontend sends a request to the [Refresh Token Route](#refresh-token-route) to retrieve a new token with the user information populated.
+
+***
+
+## Register Route
+
+The Medusa application defines an API route at `/auth/{actor_type}/{provider}/register` that creates an auth identity for an actor type, such as a `customer`. It returns a JWT token that you pass to an API route that creates the user.
+
+```bash
+curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/register
+-H 'Content-Type: application/json' \
+--data-raw '{
+ "email": "Whitney_Schultz@gmail.com"
+ // ...
+}'
+```
+
+This API route is useful for providers like `emailpass` that uses custom logic to authenticate a user. For authentication providers that authenticate with third-party services, such as Google, use the [Auth Route](#login-route) instead.
+
+For example, if you're registering a customer, you:
+
+1. Send a request to `/auth/customer/emailpass/register` to retrieve the registration JWT token.
+2. Send a request to the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers) to create the customer, passing the [JWT token in the header](https://docs.medusajs.com/api/store#authentication).
+
+### Path Parameters
+
+Its path parameters are:
+
+- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`.
+- `{provider}`: the auth provider to handle the authentication. For example, `emailpass`.
+
+### Request Body Parameters
+
+This route accepts in the request body the data that the specified authentication provider requires to handle authentication.
+
+For example, the EmailPass provider requires an `email` and `password` fields in the request body.
+
+### Response Fields
+
+If the authentication is successful, you'll receive a `token` field in the response body object:
+
+```json
+{
+ "token": "..."
+}
+```
+
+Use that token in the header of subsequent requests to send authenticated requests.
+
+### Handling Existing Identities
+
+An auth identity with the same email may already exist in Medusa. This can happen if:
+
+- Another actor type is using that email. For example, an admin user is trying to register as a customer.
+- The same email belongs to a record of the same actor type. For example, another customer has the same email.
+
+In these scenarios, the Register Route will return an error instead of a token:
+
+```json
+{
+ "type": "unauthorized",
+ "message": "Identity with email already exists"
+}
+```
+
+To handle these scenarios, you can use the [Login Route](#login-route) to validate that the email and password match the existing identity. If so, you can allow the admin user, for example, to register as a customer.
+
+Otherwise, if the email and password don't match the existing identity, such as when the email belongs to another customer, the [Login Route](#login-route) returns an error:
+
+```json
+{
+ "type": "unauthorized",
+ "message": "Invalid email or password"
+}
+```
+
+You can show that error message to the customer.
+
+***
+
+## Login Route
+
+The Medusa application defines an API route at `/auth/{actor_type}/{provider}` that authenticates a user of an actor type. It returns a JWT token that can be passed in [the header of subsequent requests](https://docs.medusajs.com/api/store#authentication) to send authenticated requests.
+
+```bash
+curl -X POST http://localhost:9000/auth/{actor_type}/{providers}
+-H 'Content-Type: application/json' \
+--data-raw '{
+ "email": "Whitney_Schultz@gmail.com"
+ // ...
+}'
+```
+
+For example, if you're authenticating a customer, you send a request to `/auth/customer/emailpass`.
+
+### Path Parameters
+
+Its path parameters are:
+
+- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`.
+- `{provider}`: the auth provider to handle the authentication. For example, `emailpass`.
+
+### Request Body Parameters
+
+This route accepts in the request body the data that the specified authentication provider requires to handle authentication.
+
+For example, the EmailPass provider requires an `email` and `password` fields in the request body.
+
+#### Overriding Callback URL
+
+For the [GitHub](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/github/index.html.md) and [Google](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/google/index.html.md) providers, you can pass a `callback_url` body parameter that overrides the `callbackUrl` set in the provider's configurations.
+
+This is useful if you want to redirect the user to a different URL after authentication based on their actor type. For example, you can set different `callback_url` for admin users and customers.
+
+### Response Fields
+
+If the authentication is successful, you'll receive a `token` field in the response body object:
+
+```json
+{
+ "token": "..."
+}
+```
+
+Use that token in the header of subsequent requests to send authenticated requests.
+
+If the authentication requires more action with a third-party service, you'll receive a `location` property:
+
+```json
+{
+ "location": "https://..."
+}
+```
+
+Redirect to that URL in the frontend to continue the authentication process with the third-party service.
+
+[How to login Customers using the authentication route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/login/index.html.md).
+
+***
+
+## Validate Callback Route
+
+The Medusa application defines an API route at `/auth/{actor_type}/{provider}/callback` that's useful for validating the authentication callback or redirect from third-party services like Google.
+
+```bash
+curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/callback?code=123&state=456
+```
+
+Refer to the [third-party authentication flow](#2-third-party-service-authenticate-flow) section to see how this route fits into the authentication flow.
+
+### Path Parameters
+
+Its path parameters are:
+
+- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`.
+- `{provider}`: the auth provider to handle the authentication. For example, `google`.
+
+### Query Parameters
+
+This route accepts all the query parameters that the third-party service sends to the frontend after the user completes the authentication process, such as the `code` and `state` query parameters.
+
+### Response Fields
+
+If the authentication is successful, you'll receive a `token` field in the response body object:
+
+```json
+{
+ "token": "..."
+}
+```
+
+In your frontend, decode the token using tools like [react-jwt](https://www.npmjs.com/package/react-jwt):
+
+- If the decoded data has an `actor_id` property, the user is already registered. So, use this token for subsequent authenticated requests.
+- If not, use the token in the header of a request that creates the user, such as the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers).
+
+***
+
+## Refresh Token Route
+
+The Medusa application defines an API route at `/auth/token/refresh` that's useful after authenticating a user with a third-party service to populate the user's token with their new information.
+
+It requires the user's JWT token that they received from the authentication or callback routes.
+
+```bash
+curl -X POST http://localhost:9000/auth/token/refresh \
+-H 'Authorization: Bearer {token}'
+```
+
+### Response Fields
+
+If the token was refreshed successfully, you'll receive a `token` field in the response body object:
+
+```json
+{
+ "token": "..."
+}
+```
+
+Use that token in the header of subsequent requests to send authenticated requests.
+
+***
+
+## Reset Password Routes
+
+To reset a user's password:
+
+1. Generate a token using the [Generate Reset Password Token API route](#generate-reset-password-token-route).
+ - The API route emits the `auth.password_reset` event, passing the token in the payload.
+ - You can create a subscriber, as seen in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/reset-password/index.html.md), that listens to the event and send a notification to the user.
+2. Pass the token to the [Reset Password API route](#reset-password-route) to reset the password.
+ - The URL in the user's notification should direct them to a frontend URL, which sends a request to this route.
+
+[Storefront Development: How to Reset a Customer's Password.](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/reset-password/index.html.md)
+
+### Generate Reset Password Token Route
+
+The Medusa application defines an API route at `/auth/{actor_type}/{auth_provider}/reset-password` that emits the `auth.password_reset` event, passing the token in the payload.
+
+```bash
+curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/reset-password
+-H 'Content-Type: application/json' \
+--data-raw '{
+ "identifier": "Whitney_Schultz@gmail.com"
+}'
+```
+
+This API route is useful for providers like `emailpass` that store a user's password and use it for authentication.
+
+#### Path Parameters
+
+Its path parameters are:
+
+- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`.
+- `{provider}`: the auth provider to handle the authentication. For example, `emailpass`.
+
+#### Request Body Parameters
+
+This route accepts in the request body an object having the following property:
+
+- `identifier`: The user's identifier in the specified auth provider. For example, for the `emailpass` auth provider, you pass the user's email.
+
+#### Response Fields
+
+If the authentication is successful, the request returns a `201` response code.
+
+### Reset Password Route
+
+The Medusa application defines an API route at `/auth/{actor_type}/{auth_provider}/update` that accepts a token and, if valid, updates the user's password.
+
+```bash
+curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/update
+-H 'Content-Type: application/json' \
+-H 'Authorization: Bearer {token}' \
+--data-raw '{
+ "email": "Whitney_Schultz@gmail.com",
+ "password": "supersecret"
+}'
+```
+
+This API route is useful for providers like `emailpass` that store a user's password and use it for logging them in.
+
+#### Path Parameters
+
+Its path parameters are:
+
+- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`.
+- `{provider}`: the auth provider to handle the authentication. For example, `emailpass`.
+
+#### Pass Token in Authorization Header
+
+Before [Medusa v2.6](https://github.com/medusajs/medusa/releases/tag/v2.6), you passed the token as a query parameter. Now, you must pass it in the `Authorization` header.
+
+In the request's authorization header, you must pass the token generated using the [Generate Reset Password Token route](#generate-reset-password-token-route). You pass it as a bearer token.
+
+### Request Body Parameters
+
+This route accepts in the request body an object that has the data necessary for the provider to update the user's password.
+
+For the `emailpass` provider, you must pass the following properties:
+
+- `email`: The user's email.
+- `password`: The new password.
+
+### Response Fields
+
+If the authentication is successful, the request returns an object with a `success` property set to `true`:
+
+```json
+{
+ "success": "true"
+}
+```
+
+
+# How to Create an Actor Type
+
+In this document, learn how to create an actor type and authenticate its associated data model.
+
+## 0. Create Module with Data Model
+
+Before creating an actor type, you must have a module with a data model representing the actor type.
+
+Learn how to create a module in [this guide](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md).
+
+The rest of this guide uses this `Manager` data model as an example:
+
+```ts title="src/modules/manager/models/manager.ts"
+import { model } from "@medusajs/framework/utils"
+
+const Manager = model.define("manager", {
+ id: model.id().primaryKey(),
+ firstName: model.text(),
+ lastName: model.text(),
+ email: model.text(),
+})
+
+export default Manager
+```
+
+***
+
+## 1. Create Workflow
+
+Start by creating a workflow that does two things:
+
+- Creates a record of the `Manager` data model.
+- Sets the `app_metadata` property of the associated `AuthIdentity` record based on the new actor type.
+
+For example, create the file `src/workflows/create-manager.ts`. with the following content:
+
+```ts title="src/workflows/create-manager.ts" highlights={workflowHighlights}
+import {
+ createWorkflow,
+ createStep,
+ StepResponse,
+ WorkflowResponse,
+} from "@medusajs/framework/workflows-sdk"
+import {
+ setAuthAppMetadataStep,
+} from "@medusajs/medusa/core-flows"
+import ManagerModuleService from "../modules/manager/service"
+
+type CreateManagerWorkflowInput = {
+ manager: {
+ first_name: string
+ last_name: string
+ email: string
+ }
+ authIdentityId: string
+}
+
+const createManagerStep = createStep(
+ "create-manager-step",
+ async ({
+ manager: managerData,
+ }: Pick,
+ { container }) => {
+ const managerModuleService: ManagerModuleService =
+ container.resolve("manager")
+
+ const manager = await managerModuleService.createManager(
+ managerData
+ )
+
+ return new StepResponse(manager)
+ }
+)
+
+const createManagerWorkflow = createWorkflow(
+ "create-manager",
+ function (input: CreateManagerWorkflowInput) {
+ const manager = createManagerStep({
+ manager: input.manager,
+ })
+
+ setAuthAppMetadataStep({
+ authIdentityId: input.authIdentityId,
+ actorType: "manager",
+ value: manager.id,
+ })
+
+ return new WorkflowResponse(manager)
+ }
+)
+
+export default createManagerWorkflow
+```
+
+This workflow accepts the manager’s data and the associated auth identity’s ID as inputs. The next sections explain how the auth identity ID is retrieved.
+
+The workflow has two steps:
+
+1. Create the manager using the `createManagerStep`.
+2. Set the `app_metadata` property of the associated auth identity using the `setAuthAppMetadataStep` from Medusa's core workflows. You specify the actor type `manager` in the `actorType` property of the step’s input.
+
+***
+
+## 2. Define the Create API Route
+
+Next, you’ll use the workflow defined in the previous section in an API route that creates a manager.
+
+So, create the file `src/api/manager/route.ts` with the following content:
+
+```ts title="src/api/manager/route.ts" highlights={createRouteHighlights}
+import type {
+ AuthenticatedMedusaRequest,
+ MedusaResponse,
+} from "@medusajs/framework/http"
+import { MedusaError } from "@medusajs/framework/utils"
+import createManagerWorkflow from "../../workflows/create-manager"
+
+type RequestBody = {
+ first_name: string
+ last_name: string
+ email: string
+}
+
+export async function POST(
+ req: AuthenticatedMedusaRequest,
+ res: MedusaResponse
+) {
+ // If `actor_id` is present, the request carries
+ // authentication for an existing manager
+ if (req.auth_context.actor_id) {
+ throw new MedusaError(
+ MedusaError.Types.INVALID_DATA,
+ "Request already authenticated as a manager."
+ )
+ }
+
+ const { result } = await createManagerWorkflow(req.scope)
+ .run({
+ input: {
+ manager: req.body,
+ authIdentityId: req.auth_context.auth_identity_id,
+ },
+ })
+
+ res.status(200).json({ manager: result })
+}
+```
+
+Since the manager must be associated with an `AuthIdentity` record, the request is expected to be authenticated, even if the manager isn’t created yet. This can be achieved by:
+
+1. Obtaining a token usng the [/auth route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route/index.html.md).
+2. Passing the token in the bearer header of the request to this route.
+
+In the API route, you create the manager using the workflow from the previous section and return it in the response.
+
+***
+
+## 3. Apply the `authenticate` Middleware
+
+The last step is to apply the `authenticate` middleware on the API routes that require a manager’s authentication.
+
+To do that, create the file `src/api/middlewares.ts` with the following content:
+
+```ts title="src/api/middlewares.ts" highlights={middlewareHighlights}
+import {
+ defineMiddlewares,
+ authenticate,
+} from "@medusajs/framework/http"
+
+export default defineMiddlewares({
+ routes: [
+ {
+ matcher: "/manager",
+ method: "POST",
+ middlewares: [
+ authenticate("manager", ["session", "bearer"], {
+ allowUnregistered: true,
+ }),
+ ],
+ },
+ {
+ matcher: "/manager/me*",
+ middlewares: [
+ authenticate("manager", ["session", "bearer"]),
+ ],
+ },
+ ],
+})
+```
+
+This applies middlewares on two route patterns:
+
+1. The `authenticate` middleware is applied on the `/manager` API route for `POST` requests while allowing unregistered managers. This requires that a bearer token be passed in the request to access the manager’s auth identity but doesn’t require the manager to be registered.
+2. The `authenticate` middleware is applied on all routes starting with `/manager/me`, restricting these routes to authenticated managers only.
+
+### Retrieve Manager API Route
+
+For example, create the file `src/api/manager/me/route.ts` with the following content:
+
+```ts title="src/api/manager/me/route.ts"
+import {
+ AuthenticatedMedusaRequest,
+ MedusaResponse,
+} from "@medusajs/framework/http"
+import ManagerModuleService from "../../../modules/manager/service"
+
+export async function GET(
+ req: AuthenticatedMedusaRequest,
+ res: MedusaResponse
+): Promise {
+ const query = req.scope.resolve("query")
+ const managerId = req.auth_context?.actor_id
+
+ const { data: [manager] } = await query.graph({
+ entity: "manager",
+ fields: ["*"],
+ filters: {
+ id: managerId,
+ },
+ }, {
+ throwIfKeyNotFound: true,
+ })
+
+ res.json({ manager })
+}
+```
+
+This route is only accessible by authenticated managers. You access the manager’s ID using `req.auth_context.actor_id`.
+
+***
+
+## Test Custom Actor Type Authentication Flow
+
+To authenticate managers:
+
+1. Send a `POST` request to `/auth/manager/emailpass/register` to create an auth identity for the manager:
+
+```bash
+curl -X POST 'http://localhost:9000/auth/manager/emailpass/register' \
+-H 'Content-Type: application/json' \
+--data-raw '{
+ "email": "manager@gmail.com",
+ "password": "supersecret"
+}'
+```
+
+Copy the returned token to use it in the next request.
+
+2. Send a `POST` request to `/manager` to create a manager:
+
+```bash
+curl -X POST 'http://localhost:9000/manager' \
+-H 'Content-Type: application/json' \
+-H 'Authorization: Bearer {token}' \
+--data-raw '{
+ "first_name": "John",
+ "last_name": "Doe",
+ "email": "manager@gmail.com"
+}'
+```
+
+Replace `{token}` with the token returned in the previous step.
+
+3. Send a `POST` request to `/auth/manager/emailpass` again to retrieve an authenticated token for the manager:
+
+```bash
+curl -X POST 'http://localhost:9000/auth/manager/emailpass' \
+-H 'Content-Type: application/json' \
+--data-raw '{
+ "email": "manager@gmail.com",
+ "password": "supersecret"
+}'
+```
+
+4. You can now send authenticated requests as a manager. For example, send a `GET` request to `/manager/me` to retrieve the authenticated manager’s details:
+
+```bash
+curl 'http://localhost:9000/manager/me' \
+-H 'Authorization: Bearer {token}'
+```
+
+Whenever you want to log in as a manager, use the `/auth/manager/emailpass` API route, as explained in step 3.
+
+***
+
+## Delete User of Actor Type
+
+When you delete a user of the actor type, you must update its auth identity to remove the association to the user.
+
+For example, create the following workflow that deletes a manager and updates its auth identity, create the file `src/workflows/delete-manager.ts` with the following content:
+
+```ts title="src/workflows/delete-manager.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports"
+import {
+ createStep,
+ StepResponse,
+} from "@medusajs/framework/workflows-sdk"
+import ManagerModuleService from "../modules/manager/service"
+
+export type DeleteManagerWorkflow = {
+ id: string
+}
+
+const deleteManagerStep = createStep(
+ "delete-manager-step",
+ async (
+ { id }: DeleteManagerWorkflow,
+ { container }) => {
+ const managerModuleService: ManagerModuleService =
+ container.resolve("manager")
+
+ const manager = await managerModuleService.retrieve(id)
+
+ await managerModuleService.deleteManagers(id)
+
+ return new StepResponse(undefined, { manager })
+ },
+ async ({ manager }, { container }) => {
+ const managerModuleService: ManagerModuleService =
+ container.resolve("manager")
+
+ await managerModuleService.createManagers(manager)
+ }
+ )
+```
+
+You add a step that deletes the manager using the `deleteManagers` method of the module's main service. In the compensation function, you create the manager again.
+
+Next, in the same file, add the workflow that deletes a manager:
+
+```ts title="src/workflows/delete-manager.ts" collapsibleLines="1-15" expandButtonLabel="Show Imports" highlights={deleteHighlights}
+// other imports
+import { MedusaError } from "@medusajs/framework/utils"
+import {
+ WorkflowData,
+ WorkflowResponse,
+ createWorkflow,
+ transform,
+} from "@medusajs/framework/workflows-sdk"
+import {
+ setAuthAppMetadataStep,
+ useQueryGraphStep,
+} from "@medusajs/medusa/core-flows"
+
+// ...
+
+export const deleteManagerWorkflow = createWorkflow(
+ "delete-manager",
+ (
+ input: WorkflowData
+ ): WorkflowResponse => {
+ deleteManagerStep(input)
+
+ const { data: authIdentities } = useQueryGraphStep({
+ entity: "auth_identity",
+ fields: ["id"],
filters: {
- id: productIds,
+ app_metadata: {
+ // the ID is of the format `{actor_type}_id`.
+ manager_id: input.id,
+ },
},
})
- const inventoryItemIds = transform({
- productsWithInventory,
- }, (data) => {
- return data.productsWithInventory.map((product) => {
- return {
- inventory_item_id: product.variants[0].inventory_items?.[0]?.inventory_item_id,
+ const authIdentity = transform(
+ { authIdentities },
+ ({ authIdentities }) => {
+ const authIdentity = authIdentities[0]
+
+ if (!authIdentity) {
+ throw new MedusaError(
+ MedusaError.Types.NOT_FOUND,
+ "Auth identity not found"
+ )
}
- })
+
+ return authIdentity
+ }
+ )
+
+ setAuthAppMetadataStep({
+ authIdentityId: authIdentity.id,
+ actorType: "manager",
+ value: null,
})
- // create bundled product
+ return new WorkflowResponse(input.id)
}
)
```
-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.
+In the workflow, you:
-Finally, create the bundled product:
+1. Use the `deleteManagerStep` defined earlier to delete the manager.
+2. Retrieve the auth identity of the manager using Query. To do that, you filter the `app_metadata` property of an auth identity, which holds the user's ID under `{actor_type_name}_id`. So, in this case, it's `manager_id`.
+3. Check that the auth identity exist, then, update the auth identity to remove the ID of the manager from it.
-```ts highlights={bundledProductHighlights3}
-export const createBundledProducts = createWorkflow(
- "create-bundled-products",
- () => {
- // ...
- const bundledProduct = createProductsWorkflow.runAsStep({
- input: {
- products: [
+You can use this workflow when deleting a manager, such as in an API route.
+
+
+# How to Handle Password Reset Token Event
+
+In this guide, you'll learn how to handle the `auth.password_reset` event, which is emitted when a request is sent to the [Generate Reset Password Token API route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#generate-reset-password-token-route/index.html.md).
+
+Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/reset-password/index.html.md) to learn how to reset your user admin password using the dashboard.
+
+You'll create a subscriber that listens to the event. When the event is emitted, the subscriber sends an email notification to the user.
+
+### Prerequisites
+
+- [A notification provider module, such as SendGrid](https://docs.medusajs.com/infrastructure-modules/notification/sendgrid/index.html.md)
+
+## 1. Create Subscriber
+
+The first step is to create a subscriber that listens to the `auth.password_reset` and sends the user a notification with instructions to reset their password.
+
+Create the file `src/subscribers/handle-reset.ts` with the following content:
+
+```ts title="src/subscribers/handle-reset.ts" highlights={highlights} collapsibleLines="1-6" expandMoreLabel="Show Imports"
+import {
+ SubscriberArgs,
+ type SubscriberConfig,
+} from "@medusajs/medusa"
+import { Modules } from "@medusajs/framework/utils"
+
+export default async function resetPasswordTokenHandler({
+ event: { data: {
+ entity_id: email,
+ token,
+ actor_type,
+ } },
+ container,
+}: SubscriberArgs<{ entity_id: string, token: string, actor_type: string }>) {
+ const notificationModuleService = container.resolve(
+ Modules.NOTIFICATION
+ )
+
+ const urlPrefix = actor_type === "customer" ?
+ "https://storefront.com" :
+ "https://admin.com/app"
+
+ await notificationModuleService.createNotifications({
+ to: email,
+ channel: "email",
+ template: "reset-password-template",
+ data: {
+ // a URL to a frontend application
+ url: `${urlPrefix}/reset-password?token=${token}&email=${email}`,
+ },
+ })
+}
+
+export const config: SubscriberConfig = {
+ event: "auth.password_reset",
+}
+```
+
+You subscribe to the `auth.password_reset` event. The event has a data payload object with the following properties:
+
+- `entity_id`: The identifier of the user. When using the `emailpass` provider, it's the user's email.
+- `token`: The token to reset the user's password.
+- `actor_type`: The user's actor type. For example, if the user is a customer, the `actor_type` is `customer`. If it's an admin user, the `actor_type` is `user`.
+
+This event's payload previously had an `actorType` field. It was renamed to `actor_type` after [Medusa v2.0.7](https://github.com/medusajs/medusa/releases/tag/v2.0.7).
+
+In the subscriber, you:
+
+- Decide the frontend URL based on whether the user is a customer or admin user by checking the value of `actor_type`.
+- Resolve the Notification Module and use its `createNotifications` method to send the notification.
+- You pass to the `createNotifications` method an object having the following properties:
+ - `to`: The identifier to send the notification to, which in this case is the email.
+ - `channel`: The channel to send the notification through, which in this case is email.
+ - `template`: The template ID in the third-party service.
+ - `data`: The data payload to pass to the template. You pass the URL to redirect the user to. You must pass the token and email in the URL so that the frontend can send them later to the Medusa application when reseting the password.
+
+***
+
+## 2. Test it Out: Generate Reset Password Token
+
+To test the subscriber out, send a request to the `/auth/{actor_type}/{auth_provider}/reset-password` API route, replacing `{actor_type}` and `{auth_provider}` with the user's actor type and provider used for authentication respectively.
+
+For example, to generate a reset password token for an admin user using the `emailpass` provider, send the following request:
+
+```bash
+curl --location 'http://localhost:9000/auth/user/emailpass/reset-password' \
+--header 'Content-Type: application/json' \
+--data-raw '{
+ "identifier": "admin-test@gmail.com"
+}'
+```
+
+In the request body, you must pass an `identifier` parameter. Its value is the user's identifier, which is the email in this case.
+
+If the token is generated successfully, the request returns a response with `201` status code. In the terminal, you'll find the following message indicating that the `auth.password_reset` event was emitted and your subscriber ran:
+
+```plain
+info: Processing auth.password_reset which has 1 subscribers
+```
+
+The notification is sent to the user with the frontend URL to enter a new password.
+
+***
+
+## Next Steps: Implementing Frontend
+
+In your frontend, you must have a page that accepts `token` and `email` query parameters.
+
+The page shows the user password fields to enter their new password, then submits the new password, token, and email to the [Reset Password Route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#reset-password-route/index.html.md).
+
+### Examples
+
+- [Storefront Guide: Reset Customer Password](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/reset-password/index.html.md)
+
+
+# Auth Module Options
+
+In this document, you'll learn about the options of the Auth Module.
+
+## providers
+
+The `providers` option is an array of auth module providers.
+
+When the Medusa application starts, these providers are registered and can be used to handle authentication.
+
+By default, the `emailpass` provider is registered to authenticate customers and admin users.
+
+For example:
+
+```ts title="medusa-config.ts"
+import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils"
+
+// ...
+
+module.exports = defineConfig({
+ // ...
+ modules: [
+ {
+ resolve: "@medusajs/medusa/auth",
+ dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER],
+ options: {
+ providers: [
{
- 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"],
- },
- ],
+ resolve: "@medusajs/medusa/auth-emailpass",
+ id: "emailpass",
+ options: {
+ // provider options...
+ },
},
],
},
- }).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
+The `providers` option is an array of objects that accept the following properties:
-```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",
- },
-})
-```
+- `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.
***
-## Stock Location Module
+## Auth CORS
-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.
+The Medusa application's authentication API routes are defined under the `/auth` prefix that requires setting the `authCors` property of the `http` configuration.
-### Retrieve with Query
+By default, the Medusa application you created will have an `AUTH_CORS` environment variable, which is used as the value of `authCors`.
-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`:
+Refer to [Medusa's configuration guide](https://docs.medusajs.com/docs/learn/configurations/medusa-config#httpauthCors/index.html.md) to learn more about the `authCors` configuration.
-### query.graph
+***
-```ts
-const { data: inventoryLevels } = await query.graph({
- entity: "inventory_level",
- fields: [
- "stock_locations.*",
- ],
-})
+## authMethodsPerActor Configuration
-// inventoryLevels[0].stock_locations
-```
+The Medusa application's configuration accept an `authMethodsPerActor` configuration which restricts the allowed auth providers used with an actor type.
-### useQueryGraphStep
-
-```ts
-import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
-
-// ...
-
-const { data: inventoryLevels } = useQueryGraphStep({
- entity: "inventory_level",
- fields: [
- "stock_locations.*",
- ],
-})
-
-// inventoryLevels[0].stock_locations
-```
+Learn more about the `authMethodsPerActor` configuration in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers#configure-allowed-auth-providers-of-actor-types/index.html.md).
# Fulfillment Concepts
@@ -24768,157 +24091,6 @@ createRemoteLinkStep({
```
-# Fulfillment Module Options
-
-In this document, you'll learn about the options of the Fulfillment Module.
-
-## providers
-
-The `providers` option is an array of fulfillment module providers.
-
-When the Medusa application starts, these providers are registered and can be used to process fulfillments.
-
-For example:
-
-```ts title="medusa-config.ts"
-import { Modules } from "@medusajs/framework/utils"
-
-// ...
-
-module.exports = defineConfig({
- // ...
- modules: [
- {
- resolve: "@medusajs/medusa/fulfillment",
- options: {
- providers: [
- {
- resolve: `@medusajs/medusa/fulfillment-manual`,
- id: "manual",
- options: {
- // provider options...
- },
- },
- ],
- },
- },
- ],
-})
-```
-
-The `providers` option is an array of objects that accept the following properties:
-
-- `resolve`: A string indicating either the package name of the module provider or the path to it relative to the `src` directory.
-- `id`: A string indicating the provider's unique name or ID.
-- `options`: An optional object of the module provider's options.
-
-
-# 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).
-
-
# Shipping Option
In this document, you’ll learn about shipping options and their rules.
@@ -24987,6 +24159,842 @@ When fulfilling an item, you might use a third-party fulfillment provider that r
The `ShippingOption` data model has a `data` property. It's an object that stores custom data relevant later when creating and processing a fulfillment.
+# 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.
+
+
+# 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
+```
+
+
+# Order Claim
+
+In this document, you’ll learn about order claims.
+
+Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/claims/index.html.md) to learn how to manage an order's claims using the dashboard.
+
+## What is a Claim?
+
+When a customer receives a defective or incorrect item, the merchant can create a claim to refund or replace the item.
+
+The [OrderClaim data model](https://docs.medusajs.com/references/order/models/OrderClaim/index.html.md) represents a claim.
+
+***
+
+## Claim Type
+
+The `Claim` data model has a `type` property whose value indicates the type of the claim:
+
+- `refund`: the items are returned, and the customer is refunded.
+- `replace`: the items are returned, and the customer receives new items.
+
+***
+
+## Old and Replacement Items
+
+When the claim is created, a return, represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md), is also created to handle receiving the old items from the customer.
+
+Learn more about returns in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md).
+
+If the claim’s type is `replace`, replacement items are represented by the [ClaimItem data model](https://docs.medusajs.com/references/order/models/OrderClaimItem/index.html.md).
+
+***
+
+## Claim Shipping Methods
+
+A claim uses shipping methods to send the replacement items to the customer. These methods are represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md).
+
+The shipping methods for the returned items are associated with the claim's return, as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return#return-shipping-methods/index.html.md).
+
+***
+
+## Claim Refund
+
+If the claim’s type is `refund`, the amount to be refunded is stored in the `refund_amount` property.
+
+The [Transaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md) represents the refunds made for the claim.
+
+***
+
+## How Claims Impact an Order’s Version
+
+When a claim is confirmed, the order’s version is incremented.
+
+
+# Order Concepts
+
+In this document, you’ll learn about orders and related concepts
+
+## 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 Exchange
+
+In this document, you’ll learn about order exchanges.
+
+Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/exchanges/index.html.md) to learn how to manage an order's exchanges using the dashboard.
+
+## What is an Exchange?
+
+An exchange is the replacement of an item that the customer ordered with another.
+
+A merchant creates the exchange, specifying the items to be replaced and the new items to be sent.
+
+The [OrderExchange data model](https://docs.medusajs.com/references/order/models/OrderExchange/index.html.md) represents an exchange.
+
+***
+
+## Returned and New Items
+
+When the exchange is created, a return, represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md), is created to handle receiving the items back from the customer.
+
+Learn more about returns in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md).
+
+The [OrderExchangeItem data model](https://docs.medusajs.com/references/order/models/OrderExchangeItem/index.html.md) represents the new items to be sent to the customer.
+
+***
+
+## Exchange Shipping Methods
+
+An exchange has shipping methods used to send the new items to the customer. They’re represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md).
+
+The shipping methods for the returned items are associated with the exchange's return, as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return#return-shipping-methods/index.html.md).
+
+***
+
+## Exchange Payment
+
+The `Exchange` data model has a `difference_due` property that stores the outstanding amount.
+
+|Condition|Result|
+|---|---|---|
+|\`difference\_due \< 0\`|Merchant owes the customer a refund of the |
+|\`difference\_due > 0\`|Merchant requires additional payment from the customer of the |
+|\`difference\_due = 0\`|No payment processing is required.|
+
+Any payment or refund made is stored in the [Transaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md).
+
+***
+
+## How Exchanges Impact an Order’s Version
+
+When an exchange is confirmed, the order’s version is incremented.
+
+
# Links between Order Module and Other Modules
This document showcases the module links defined between the Order Module and other Commerce Modules.
@@ -25511,36 +25519,6 @@ 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.
@@ -25580,6 +25558,154 @@ 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 Edit
+
+In this document, you'll learn about order edits.
+
+Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/edit/index.html.md) to learn how to edit an order's items using the dashboard.
+
+## What is an Order Edit?
+
+A merchant can edit an order to add new items or change the quantity of existing items in the order.
+
+An order edit is represented by the [OrderChange data model](https://docs.medusajs.com/references/order/models/OrderChange/index.html.md).
+
+The `OrderChange` data model is associated with any type of change, including a return or exchange. However, its `change_type` property distinguishes the type of change it's making.
+
+In the case of an order edit, the `OrderChange`'s type is `edit`.
+
+***
+
+## Add Items in an Order Edit
+
+When the merchant adds new items to the order in the order edit, the item is added as an [OrderItem](https://docs.medusajs.com/references/order/models/OrderItem/index.html.md).
+
+Also, an `OrderChangeAction` is created. The [OrderChangeAction data model](https://docs.medusajs.com/references/order/models/OrderChangeAction/index.html.md) represents a change made by an `OrderChange`, such as an item added.
+
+So, when an item is added, an `OrderChangeAction` is created with the type `ITEM_ADD`. In its `details` property, the item's ID, price, and quantity are stored.
+
+***
+
+## Update Items in an Order Edit
+
+A merchant can update an existing item's quantity or price.
+
+This change is added as an `OrderChangeAction` with the type `ITEM_UPDATE`. In its `details` property, the item's ID, new price, and new quantity are stored.
+
+***
+
+## Shipping Methods of New Items in the Edit
+
+Adding new items to the order requires adding shipping methods for those items.
+
+These shipping methods are represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderItem/index.html.md). Also, an `OrderChangeAction` is created with the type `SHIPPING_ADD`
+
+***
+
+## How Order Edits Impact an Order’s Version
+
+When an order edit is confirmed, the order’s version is incremented.
+
+***
+
+## Payments and Refunds for Order Edit Changes
+
+Once the Order Edit is confirmed, any additional payment or refund required can be made on the original order.
+
+This is determined by the comparison between the `OrderSummary` and the order's transactions, as mentioned in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/transactions#checking-outstanding-amount/index.html.md).
+
+
+# Order Versioning
+
+In this document, you’ll learn how an order and its details are versioned.
+
+## 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 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](references/order/models/ReturnItem).
+
+The `ReturnItem` model has two properties storing the item's quantity:
+
+1. `received_quantity`: The quantity of the item that's received and can be added to the item's inventory quantity.
+2. `damaged_quantity`: The quantity of the item that's damaged, meaning it can't be sold again or added to the item's inventory quantity.
+
+***
+
+## Return Shipping Methods
+
+A return has shipping methods used to return the items to the merchant. The shipping methods are represented by the [OrderShippingMethod data model](references/order/models/OrderShippingMethod).
+
+In the Medusa application, the shipping method for a return is created only from a shipping option, provided by the Fulfillment Module, that has the rule `is_return` enabled.
+
+***
+
+## Refund Payment
+
+The `refund_amount` property of the `Return` data model holds the amount a merchant must refund the customer.
+
+The [OrderTransaction data model](references/order/models/OrderTransaction) represents the refunds made for the return.
+
+***
+
+## Returns in Exchanges and Claims
+
+When a merchant creates an exchange or a claim, it includes returning items from the customer.
+
+The `Return` data model also represents the return of these items. In this case, the return is associated with the exchange or claim it was created for.
+
+***
+
+## How Returns Impact an Order’s Version
+
+The order’s version is incremented when:
+
+1. A return is requested.
+2. A return is marked as received.
+
+
# Promotions Adjustments in Orders
In this document, you’ll learn how a promotion is applied to an order’s items and shipping methods using adjustment lines.
@@ -25836,6 +25962,976 @@ const { data: stores } = useQueryGraphStep({
```
+# Customer Accounts
+
+In this document, you’ll learn how registered and unregistered accounts are distinguished in the Medusa application.
+
+Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/customers/index.html.md) to learn how to manage customers using the dashboard.
+
+## `has_account` Property
+
+The [Customer data model](https://docs.medusajs.com/references/customer/models/Customer/index.html.md) has a `has_account` property, which is a boolean that indicates whether a customer is registered.
+
+When a guest customer places an order, a new `Customer` record is created with `has_account` set to `false`.
+
+When this or another guest customer registers an account with the same email, a new `Customer` record is created with `has_account` set to `true`.
+
+***
+
+## Email Uniqueness
+
+The above behavior means that two `Customer` records may exist with the same email. However, the main difference is the `has_account` property's value.
+
+So, there can only be one guest customer (having `has_account=false`) and one registered customer (having `has_account=true`) with the same email.
+
+
+# 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
+```
+
+
+# Pricing Concepts
+
+In this document, you’ll learn about the main concepts in the Pricing Module.
+
+## Price Set
+
+A [PriceSet](https://docs.medusajs.com/references/pricing/models/PriceSet/index.html.md) represents a collection of prices that are linked to a resource (for example, a product or a shipping option).
+
+Each of these prices are represented by the [Price data module](https://docs.medusajs.com/references/pricing/models/Price/index.html.md).
+
+
+
+***
+
+## Price List
+
+A [PriceList](https://docs.medusajs.com/references/pricing/models/PriceList/index.html.md) is a group of prices only enabled if their conditions and rules are satisfied.
+
+A price list has optional `start_date` and `end_date` properties that indicate the date range in which a price list can be applied.
+
+Its associated prices are represented by the `Price` data model.
+
+
+# Links between Pricing Module and Other Modules
+
+This document showcases the module links defined between the Pricing Module and other Commerce Modules.
+
+## Summary
+
+The Pricing Module has the following links to other modules:
+
+|First Data Model|Second Data Model|Type|Description|
+|---|---|---|---|
+|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.
+
+## Tiered Pricing
+
+Each price, represented by the [Price data model](https://docs.medusajs.com/references/pricing/models/Price/index.html.md), has two optional properties that can be used to create tiered prices:
+
+- `min_quantity`: The minimum quantity that must be in the cart for the price to be applied.
+- `max_quantity`: The maximum quantity that can be in the cart for the price to be applied.
+
+This is useful to set tiered pricing for resources like product variants and shipping options.
+
+For example, you can set a variant's price to:
+
+- `$10` by default.
+- `$8` when the customer adds `10` or more of the variant to the cart.
+- `$6` when the customer adds `20` or more of the variant to the cart.
+
+These price definitions would look like this:
+
+```json title="Example Prices"
+[
+ // default price
+ {
+ "amount": 10,
+ "currency_code": "usd",
+ },
+ {
+ "amount": 8,
+ "currency_code": "usd",
+ "min_quantity": 10,
+ "max_quantity": 19,
+ },
+ {
+ "amount": 6,
+ "currency_code": "usd",
+ "min_quantity": 20,
+ },
+],
+```
+
+### How to Create Tiered Prices?
+
+When you create prices, you can specify a `min_quantity` and `max_quantity` for each price. This allows you to create tiered pricing, where the price changes based on the quantity of items in the cart.
+
+For example:
+
+For most use cases where you're building customizations in the Medusa application, it's highly recommended to use [Medusa's workflows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/medusa-workflows-reference/index.html.md) rather than using the Pricing Module directly. Medusa's workflows already implement extensive functionalities that you can re-use in your custom flows, with reliable roll-back mechanism.
+
+### Using Medusa Workflows
+
+```ts highlights={tieredPricingHighlights}
+const { result } = await createProductsWorkflow(container)
+ .run({
+ input: {
+ products: [{
+ variants: [{
+ id: "variant_1",
+ prices: [
+ // default price
+ {
+ amount: 10,
+ currency_code: "usd",
+ },
+ {
+ amount: 8,
+ currency_code: "usd",
+ min_quantity: 10,
+ max_quantity: 19,
+ },
+ {
+ amount: 6,
+ currency_code: "usd",
+ min_quantity: 20,
+ },
+ ],
+ // ...
+ }],
+ }],
+ // ...
+ },
+ })
+```
+
+### Using the Pricing Module
+
+```ts
+const priceSet = await pricingModule.addPrices({
+ priceSetId: "pset_1",
+ prices: [
+ // default price
+ {
+ amount: 10,
+ currency_code: "usd",
+ },
+ // tiered prices
+ {
+ amount: 8,
+ currency_code: "usd",
+ min_quantity: 10,
+ max_quantity: 19,
+ },
+ {
+ amount: 6,
+ currency_code: "usd",
+ min_quantity: 20,
+ },
+ ],
+})
+```
+
+In this example, you create a product with a variant whose default price is `$10`. You also add two tiered prices that set the price to `$8` when the quantity is between `10` and `19`, and to `$6` when the quantity is `20` or more.
+
+### How are Tiered Prices Applied?
+
+The [price calculation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md) mechanism considers the cart's items as a context when choosing the best price to apply.
+
+For example, consider the customer added the `variant_1` product variant (created in the workflow snippet of the [above section](#how-to-create-tiered-prices)) to their cart with a quantity of `15`.
+
+The price calculation mechanism will choose the second price, which is `$8`, because the quantity of `15` is between `10` and `19`.
+
+If there are other rules applied to the price, they may affect the price calculation. Keep reading to learn about other price rules, and refer to the [Price Calculation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md) guide for more details on the calculation mechanism.
+
+***
+
+## Price Rule
+
+You can also restrict prices by advanced rules, such as a customer's group, zip code, or a cart's total.
+
+Each rule of a price is represented by the [PriceRule data model](https://docs.medusajs.com/references/pricing/models/PriceRule/index.html.md).
+
+The `Price` data model has a `rules_count` property, which indicates how many rules, represented by `PriceRule`, are applied to the price.
+
+For exmaple, you create a price restricted to `10557` zip codes.
+
+
+
+A price can have multiple price rules.
+
+For example, a price can be restricted by a region and a zip code.
+
+
+
+### Price List Rules
+
+Rules applied to a price list are represented by the [PriceListRule data model](https://docs.medusajs.com/references/pricing/models/PriceListRule/index.html.md).
+
+The `rules_count` property of a `PriceList` indicates how many rules are applied to it.
+
+
+
+### How to Create Prices with Rules?
+
+When you create prices, you can specify rules for each price. This allows you to create complex pricing strategies based on different contexts.
+
+For example:
+
+For most use cases where you're building customizations in the Medusa application, it's highly recommended to use [Medusa's workflows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/medusa-workflows-reference/index.html.md) rather than using the Pricing Module directly. Medusa's workflows already implement extensive functionalities that you can re-use in your custom flows, with reliable roll-back mechanism.
+
+### Using Medusa Workflows
+
+```ts highlights={workflowHighlights}
+const { result } = await createShippingOptionsWorkflow(container)
+ .run({
+ input: [{
+ name: "Standard Shipping",
+ service_zone_id: "serzo_123",
+ shipping_profile_id: "sp_123",
+ provider_id: "prov_123",
+ type: {
+ label: "Standard",
+ description: "Standard shipping",
+ code: "standard",
+ },
+ price_type: "flat",
+ prices: [
+ // default price
+ {
+ currency_code: "usd",
+ amount: 10,
+ rules: {},
+ },
+ // price if cart total >= $100
+ {
+ currency_code: "usd",
+ amount: 0,
+ rules: {
+ item_total: {
+ operator: "gte",
+ value: 100,
+ },
+ },
+ },
+ ],
+ }],
+ })
+```
+
+### Using the Pricing Module
+
+```ts
+const priceSet = await pricingModule.addPrices({
+ priceSetId: "pset_1",
+ prices: [
+ // default price
+ {
+ currency_code: "usd",
+ amount: 10,
+ rules: {},
+ },
+ // price if cart total >= $100
+ {
+ currency_code: "usd",
+ amount: 0,
+ rules: {
+ item_total: {
+ operator: "gte",
+ value: 100,
+ },
+ },
+ },
+ ],
+})
+```
+
+In this example, you create a shipping option whose default price is `$10`. When the total of the cart or order using this shipping option is greater than `$100`, the shipping option's price becomes free.
+
+### How is the Price Rule Applied?
+
+The [price calculation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md) mechanism considers a price applicable when the resource that this price is in matches the specified rules.
+
+For example, a [cart object](https://docs.medusajs.com/api/store#carts_cart_schema) has an `item_total` property. So, if a shipping option has the following price:
+
+```json
+{
+ "currency_code": "usd",
+ "amount": 0,
+ "rules": {
+ "item_total": {
+ "operator": "gte",
+ "value": 100,
+ }
+ }
+}
+```
+
+The shipping option's price is applied when the cart's `item_total` is greater than or equal to `$100`.
+
+You can also apply the rule on nested relations and properties. For example, to apply a shipping option's price based on the customer's group, you can apply a rule on the `customer.group.id` attribute:
+
+```json
+{
+ "currency_code": "usd",
+ "amount": 0,
+ "rules": {
+ "customer.group.id": {
+ "operator": "eq",
+ "value": "cusgrp_123"
+ }
+ }
+}
+```
+
+In this example, the price is only applied if a cart's customer belongs to the customer group of ID `cusgrp_123`.
+
+These same rules apply to product variant prices as well, or any other resource that has a price.
+
+
+# Prices Calculation
+
+In this document, you'll learn how prices are calculated when you use the [calculatePrices method](https://docs.medusajs.com/references/pricing/calculatePrices/index.html.md) of the Pricing Module's main service.
+
+## calculatePrices Method
+
+The [calculatePrices method](https://docs.medusajs.com/references/pricing/calculatePrices/index.html.md) accepts as parameters the ID of one or more price sets and a context.
+
+It returns a price object with the best matching price for each price set.
+
+### Calculation Context
+
+The calculation context is an optional object passed as a second parameter to the `calculatePrices` method. It accepts rules to restrict the selected prices in the price set.
+
+For example:
+
+```ts
+const price = await pricingModuleService.calculatePrices(
+ { id: [priceSetId] },
+ {
+ context: {
+ currency_code: currencyCode,
+ region_id: "reg_123",
+ },
+ }
+)
+```
+
+In this example, you retrieve the prices in a price set for the specified currency code and region ID.
+
+### Returned Price Object
+
+For each price set, the `calculatePrices` method selects two prices:
+
+- A calculated price: Either a price that belongs to a price list and best matches the specified context, or the same as the original price.
+- An original price, which is either:
+ - The same price as the calculated price if the price list it belongs to is of type `override`;
+ - Or a price that doesn't belong to a price list and best matches the specified context.
+
+Both prices are returned in an object that has the following properties:
+
+- id: (\`string\`) The ID of the price set from which the price was selected.
+- is\_calculated\_price\_price\_list: (\`boolean\`) Whether the calculated price belongs to a price list.
+- calculated\_amount: (\`number\`) The amount of the calculated price, or \`null\` if there isn't a calculated price. This is the amount shown to the customer.
+- is\_original\_price\_price\_list: (\`boolean\`) Whether the original price belongs to a price list.
+- original\_amount: (\`number\`) The amount of the original price, or \`null\` if there isn't an original price. This amount is useful to compare with the \`calculated\_amount\`, such as to check for discounted value.
+- currency\_code: (\`string\`) The currency code of the calculated price, or \`null\` if there isn't a calculated price.
+- is\_calculated\_price\_tax\_inclusive: (\`boolean\`) Whether the calculated price is tax inclusive. Learn more about tax-inclusivity in \[this document]\(../tax-inclusive-pricing/page.mdx)
+- is\_original\_price\_tax\_inclusive: (\`boolean\`) Whether the original price is tax inclusive. Learn more about tax-inclusivity in \[this document]\(../tax-inclusive-pricing/page.mdx)
+- calculated\_price: (\`object\`) The calculated price's price details.
+
+ - id: (\`string\`) The ID of the price.
+
+ - price\_list\_id: (\`string\`) The ID of the associated price list.
+
+ - price\_list\_type: (\`string\`) The price list's type. For example, \`sale\`.
+
+ - min\_quantity: (\`number\`) The price's min quantity condition.
+
+ - max\_quantity: (\`number\`) The price's max quantity condition.
+- original\_price: (\`object\`) The original price's price details.
+
+ - id: (\`string\`) The ID of the price.
+
+ - price\_list\_id: (\`string\`) The ID of the associated price list.
+
+ - price\_list\_type: (\`string\`) The price list's type. For example, \`sale\`.
+
+ - min\_quantity: (\`number\`) The price's min quantity condition.
+
+ - max\_quantity: (\`number\`) The price's max quantity condition.
+
+***
+
+## Examples
+
+Consider the following price set:
+
+```ts
+const priceSet = await pricingModuleService.createPriceSets({
+ prices: [
+ // default price
+ {
+ amount: 500,
+ currency_code: "EUR",
+ rules: {},
+ },
+ // prices with rules
+ {
+ amount: 400,
+ currency_code: "EUR",
+ rules: {
+ region_id: "reg_123",
+ },
+ },
+ {
+ amount: 450,
+ currency_code: "EUR",
+ rules: {
+ city: "krakow",
+ },
+ },
+ {
+ amount: 500,
+ currency_code: "EUR",
+ rules: {
+ city: "warsaw",
+ region_id: "reg_123",
+ },
+ },
+ {
+ amount: 200,
+ currency_code: "EUR",
+ min_quantity: 100,
+ },
+ ],
+})
+```
+
+### Default Price Selection
+
+### Code
+
+```ts
+const price = await pricingModuleService.calculatePrices(
+ { id: [priceSet.id] },
+ {
+ context: {
+ currency_code: "EUR"
+ }
+ }
+)
+```
+
+### Result
+
+### Calculate Prices with Rules
+
+### Code
+
+```ts
+const price = await pricingModuleService.calculatePrices(
+ { id: [priceSet.id] },
+ {
+ context: {
+ currency_code: "EUR",
+ region_id: "reg_123",
+ city: "krakow"
+ }
+ }
+)
+```
+
+### Result
+
+### Tiered Pricing Selection
+
+### Code
+
+```ts
+const price = await pricingModuleService.calculatePrices(
+ { id: [priceSet.id] },
+ {
+ context: {
+ cart: {
+ items: [
+ {
+ id: "item_1",
+ quantity: 200,
+ // assuming the price set belongs to this variant
+ variant_id: "variant_1",
+ // ...
+ }
+ ],
+ // ...
+ }
+ }
+ }
+)
+```
+
+### Result
+
+### Price Selection with Price List
+
+### Code
+
+```ts
+const priceList = pricingModuleService.createPriceLists([{
+ title: "Summer Price List",
+ description: "Price list for summer sale",
+ starts_at: Date.parse("01/10/2023").toString(),
+ ends_at: Date.parse("31/10/2023").toString(),
+ rules: {
+ region_id: ['PL']
+ },
+ type: "sale",
+ prices: [
+ {
+ amount: 400,
+ currency_code: "EUR",
+ price_set_id: priceSet.id,
+ },
+ {
+ amount: 450,
+ currency_code: "EUR",
+ price_set_id: priceSet.id,
+ },
+ ],
+}]);
+
+const price = await pricingModuleService.calculatePrices(
+ { id: [priceSet.id] },
+ {
+ context: {
+ currency_code: "EUR",
+ region_id: "PL",
+ city: "krakow"
+ }
+ }
+)
+```
+
+### Result
+
+
+# Tax-Inclusive Pricing
+
+In this document, you’ll learn about tax-inclusive pricing and how it's used when calculating prices.
+
+## What is Tax-Inclusive Pricing?
+
+A tax-inclusive price is a price of a resource that includes taxes. Medusa calculates the tax amount from the price rather than adds the amount to it.
+
+For example, if a product’s price is $50, the tax rate is 2%, and tax-inclusive pricing is enabled, then the product's price is $49, and the applied tax amount is $1.
+
+***
+
+## How is Tax-Inclusive Pricing Set?
+
+The [PricePreference data model](https://docs.medusajs.com/references/pricing/models/PricePreference/index.html.md) holds the tax-inclusive setting for a context. It has two properties that indicate the context:
+
+- `attribute`: The name of the attribute to compare against. For example, `region_id` or `currency_code`.
+- `value`: The attribute’s value. For example, `reg_123` or `usd`.
+
+Only `region_id` and `currency_code` are supported as an `attribute` at the moment.
+
+The `is_tax_inclusive` property indicates whether tax-inclusivity is enabled in the specified context.
+
+For example:
+
+```json
+{
+ "attribute": "currency_code",
+ "value": "USD",
+ "is_tax_inclusive": true,
+}
+```
+
+In this example, tax-inclusivity is enabled for the `USD` currency code.
+
+***
+
+## Tax-Inclusive Pricing in Price Calculation
+
+### Tax Context
+
+As mentioned in the [Price Calculation documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md), The `calculatePrices` method accepts as a parameter a calculation context.
+
+To get accurate tax results, pass the `region_id` and / or `currency_code` in the calculation context.
+
+### Returned Tax Properties
+
+The `calculatePrices` method returns two properties related to tax-inclusivity:
+
+Learn more about the returned properties in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#returned-price-object/index.html.md).
+
+- `is_calculated_price_tax_inclusive`: Whether the selected `calculated_price` is tax-inclusive.
+- `is_original_price_tax_inclusive` : Whether the selected `original_price` is tax-inclusive.
+
+A price is considered tax-inclusive if:
+
+1. It belongs to the region or currency code specified in the calculation context;
+2. and the region or currency code has a price preference with `is_tax_inclusive` enabled.
+
+### Tax Context Precedence
+
+A region’s price preference’s `is_tax_inclusive`'s value takes higher precedence in determining whether a price is tax-inclusive if:
+
+- both the `region_id` and `currency_code` are provided in the calculation context;
+- the selected price belongs to the region;
+- and the region has a price preference
+
+
# 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.
@@ -26231,61 +27327,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.
@@ -26375,6 +27416,61 @@ You can also build a custom payment flow using workflows or the Payment Module's
Refer to the [Accept Payment Flow](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-flow/index.html.md) guide to learn more.
+# Payment 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 Collection
In this document, you’ll learn what a payment collection is and how the Medusa application uses it with the Cart Module.
@@ -26789,1124 +27885,6 @@ View the full flow of the webhook event processing in the [processPaymentWorkflo
- In either cases, if the cart associated with the payment session is not completed yet, Medusa will complete the cart.
-# 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](references/order/models/ReturnItem).
-
-The `ReturnItem` model has two properties storing the item's quantity:
-
-1. `received_quantity`: The quantity of the item that's received and can be added to the item's inventory quantity.
-2. `damaged_quantity`: The quantity of the item that's damaged, meaning it can't be sold again or added to the item's inventory quantity.
-
-***
-
-## Return Shipping Methods
-
-A return has shipping methods used to return the items to the merchant. The shipping methods are represented by the [OrderShippingMethod data model](references/order/models/OrderShippingMethod).
-
-In the Medusa application, the shipping method for a return is created only from a shipping option, provided by the Fulfillment Module, that has the rule `is_return` enabled.
-
-***
-
-## Refund Payment
-
-The `refund_amount` property of the `Return` data model holds the amount a merchant must refund the customer.
-
-The [OrderTransaction data model](references/order/models/OrderTransaction) represents the refunds made for the return.
-
-***
-
-## Returns in Exchanges and Claims
-
-When a merchant creates an exchange or a claim, it includes returning items from the customer.
-
-The `Return` data model also represents the return of these items. In this case, the return is associated with the exchange or claim it was created for.
-
-***
-
-## How Returns Impact an Order’s Version
-
-The order’s version is incremented when:
-
-1. A return is requested.
-2. A return is marked as received.
-
-
-# Order 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 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.
-
-
-# Pricing Concepts
-
-In this document, you’ll learn about the main concepts in the Pricing Module.
-
-## Price Set
-
-A [PriceSet](https://docs.medusajs.com/references/pricing/models/PriceSet/index.html.md) represents a collection of prices that are linked to a resource (for example, a product or a shipping option).
-
-Each of these prices are represented by the [Price data module](https://docs.medusajs.com/references/pricing/models/Price/index.html.md).
-
-
-
-***
-
-## Price List
-
-A [PriceList](https://docs.medusajs.com/references/pricing/models/PriceList/index.html.md) is a group of prices only enabled if their conditions and rules are satisfied.
-
-A price list has optional `start_date` and `end_date` properties that indicate the date range in which a price list can be applied.
-
-Its associated prices are represented by the `Price` data model.
-
-
-# Prices Calculation
-
-In this document, you'll learn how prices are calculated when you use the [calculatePrices method](https://docs.medusajs.com/references/pricing/calculatePrices/index.html.md) of the Pricing Module's main service.
-
-## calculatePrices Method
-
-The [calculatePrices method](https://docs.medusajs.com/references/pricing/calculatePrices/index.html.md) accepts as parameters the ID of one or more price sets and a context.
-
-It returns a price object with the best matching price for each price set.
-
-### Calculation Context
-
-The calculation context is an optional object passed as a second parameter to the `calculatePrices` method. It accepts rules to restrict the selected prices in the price set.
-
-For example:
-
-```ts
-const price = await pricingModuleService.calculatePrices(
- { id: [priceSetId] },
- {
- context: {
- currency_code: currencyCode,
- region_id: "reg_123",
- },
- }
-)
-```
-
-In this example, you retrieve the prices in a price set for the specified currency code and region ID.
-
-### Returned Price Object
-
-For each price set, the `calculatePrices` method selects two prices:
-
-- A calculated price: Either a price that belongs to a price list and best matches the specified context, or the same as the original price.
-- An original price, which is either:
- - The same price as the calculated price if the price list it belongs to is of type `override`;
- - Or a price that doesn't belong to a price list and best matches the specified context.
-
-Both prices are returned in an object that has the following properties:
-
-- id: (\`string\`) The ID of the price set from which the price was selected.
-- is\_calculated\_price\_price\_list: (\`boolean\`) Whether the calculated price belongs to a price list.
-- calculated\_amount: (\`number\`) The amount of the calculated price, or \`null\` if there isn't a calculated price. This is the amount shown to the customer.
-- is\_original\_price\_price\_list: (\`boolean\`) Whether the original price belongs to a price list.
-- original\_amount: (\`number\`) The amount of the original price, or \`null\` if there isn't an original price. This amount is useful to compare with the \`calculated\_amount\`, such as to check for discounted value.
-- currency\_code: (\`string\`) The currency code of the calculated price, or \`null\` if there isn't a calculated price.
-- is\_calculated\_price\_tax\_inclusive: (\`boolean\`) Whether the calculated price is tax inclusive. Learn more about tax-inclusivity in \[this document]\(../tax-inclusive-pricing/page.mdx)
-- is\_original\_price\_tax\_inclusive: (\`boolean\`) Whether the original price is tax inclusive. Learn more about tax-inclusivity in \[this document]\(../tax-inclusive-pricing/page.mdx)
-- calculated\_price: (\`object\`) The calculated price's price details.
-
- - id: (\`string\`) The ID of the price.
-
- - price\_list\_id: (\`string\`) The ID of the associated price list.
-
- - price\_list\_type: (\`string\`) The price list's type. For example, \`sale\`.
-
- - min\_quantity: (\`number\`) The price's min quantity condition.
-
- - max\_quantity: (\`number\`) The price's max quantity condition.
-- original\_price: (\`object\`) The original price's price details.
-
- - id: (\`string\`) The ID of the price.
-
- - price\_list\_id: (\`string\`) The ID of the associated price list.
-
- - price\_list\_type: (\`string\`) The price list's type. For example, \`sale\`.
-
- - min\_quantity: (\`number\`) The price's min quantity condition.
-
- - max\_quantity: (\`number\`) The price's max quantity condition.
-
-***
-
-## Examples
-
-Consider the following price set:
-
-```ts
-const priceSet = await pricingModuleService.createPriceSets({
- prices: [
- // default price
- {
- amount: 500,
- currency_code: "EUR",
- rules: {},
- },
- // prices with rules
- {
- amount: 400,
- currency_code: "EUR",
- rules: {
- region_id: "reg_123",
- },
- },
- {
- amount: 450,
- currency_code: "EUR",
- rules: {
- city: "krakow",
- },
- },
- {
- amount: 500,
- currency_code: "EUR",
- rules: {
- city: "warsaw",
- region_id: "reg_123",
- },
- },
- {
- amount: 200,
- currency_code: "EUR",
- min_quantity: 100,
- },
- ],
-})
-```
-
-### Default Price Selection
-
-### Code
-
-```ts
-const price = await pricingModuleService.calculatePrices(
- { id: [priceSet.id] },
- {
- context: {
- currency_code: "EUR"
- }
- }
-)
-```
-
-### Result
-
-### Calculate Prices with Rules
-
-### Code
-
-```ts
-const price = await pricingModuleService.calculatePrices(
- { id: [priceSet.id] },
- {
- context: {
- currency_code: "EUR",
- region_id: "reg_123",
- city: "krakow"
- }
- }
-)
-```
-
-### Result
-
-### Tiered Pricing Selection
-
-### Code
-
-```ts
-const price = await pricingModuleService.calculatePrices(
- { id: [priceSet.id] },
- {
- context: {
- cart: {
- items: [
- {
- id: "item_1",
- quantity: 200,
- // assuming the price set belongs to this variant
- variant_id: "variant_1",
- // ...
- }
- ],
- // ...
- }
- }
- }
-)
-```
-
-### Result
-
-### Price Selection with Price List
-
-### Code
-
-```ts
-const priceList = pricingModuleService.createPriceLists([{
- title: "Summer Price List",
- description: "Price list for summer sale",
- starts_at: Date.parse("01/10/2023").toString(),
- ends_at: Date.parse("31/10/2023").toString(),
- rules: {
- region_id: ['PL']
- },
- type: "sale",
- prices: [
- {
- amount: 400,
- currency_code: "EUR",
- price_set_id: priceSet.id,
- },
- {
- amount: 450,
- currency_code: "EUR",
- price_set_id: priceSet.id,
- },
- ],
-}]);
-
-const price = await pricingModuleService.calculatePrices(
- { id: [priceSet.id] },
- {
- context: {
- currency_code: "EUR",
- region_id: "PL",
- city: "krakow"
- }
- }
-)
-```
-
-### Result
-
-
-# Price Tiers and Rules
-
-In this Pricing Module guide, you'll learn about tired prices, price rules for price sets and price lists, and how to add rules to a price.
-
-## Tiered Pricing
-
-Each price, represented by the [Price data model](https://docs.medusajs.com/references/pricing/models/Price/index.html.md), has two optional properties that can be used to create tiered prices:
-
-- `min_quantity`: The minimum quantity that must be in the cart for the price to be applied.
-- `max_quantity`: The maximum quantity that can be in the cart for the price to be applied.
-
-This is useful to set tiered pricing for resources like product variants and shipping options.
-
-For example, you can set a variant's price to:
-
-- `$10` by default.
-- `$8` when the customer adds `10` or more of the variant to the cart.
-- `$6` when the customer adds `20` or more of the variant to the cart.
-
-These price definitions would look like this:
-
-```json title="Example Prices"
-[
- // default price
- {
- "amount": 10,
- "currency_code": "usd",
- },
- {
- "amount": 8,
- "currency_code": "usd",
- "min_quantity": 10,
- "max_quantity": 19,
- },
- {
- "amount": 6,
- "currency_code": "usd",
- "min_quantity": 20,
- },
-],
-```
-
-### How to Create Tiered Prices?
-
-When you create prices, you can specify a `min_quantity` and `max_quantity` for each price. This allows you to create tiered pricing, where the price changes based on the quantity of items in the cart.
-
-For example:
-
-For most use cases where you're building customizations in the Medusa application, it's highly recommended to use [Medusa's workflows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/medusa-workflows-reference/index.html.md) rather than using the Pricing Module directly. Medusa's workflows already implement extensive functionalities that you can re-use in your custom flows, with reliable roll-back mechanism.
-
-### Using Medusa Workflows
-
-```ts highlights={tieredPricingHighlights}
-const { result } = await createProductsWorkflow(container)
- .run({
- input: {
- products: [{
- variants: [{
- id: "variant_1",
- prices: [
- // default price
- {
- amount: 10,
- currency_code: "usd",
- },
- {
- amount: 8,
- currency_code: "usd",
- min_quantity: 10,
- max_quantity: 19,
- },
- {
- amount: 6,
- currency_code: "usd",
- min_quantity: 20,
- },
- ],
- // ...
- }],
- }],
- // ...
- },
- })
-```
-
-### Using the Pricing Module
-
-```ts
-const priceSet = await pricingModule.addPrices({
- priceSetId: "pset_1",
- prices: [
- // default price
- {
- amount: 10,
- currency_code: "usd",
- },
- // tiered prices
- {
- amount: 8,
- currency_code: "usd",
- min_quantity: 10,
- max_quantity: 19,
- },
- {
- amount: 6,
- currency_code: "usd",
- min_quantity: 20,
- },
- ],
-})
-```
-
-In this example, you create a product with a variant whose default price is `$10`. You also add two tiered prices that set the price to `$8` when the quantity is between `10` and `19`, and to `$6` when the quantity is `20` or more.
-
-### How are Tiered Prices Applied?
-
-The [price calculation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md) mechanism considers the cart's items as a context when choosing the best price to apply.
-
-For example, consider the customer added the `variant_1` product variant (created in the workflow snippet of the [above section](#how-to-create-tiered-prices)) to their cart with a quantity of `15`.
-
-The price calculation mechanism will choose the second price, which is `$8`, because the quantity of `15` is between `10` and `19`.
-
-If there are other rules applied to the price, they may affect the price calculation. Keep reading to learn about other price rules, and refer to the [Price Calculation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md) guide for more details on the calculation mechanism.
-
-***
-
-## Price Rule
-
-You can also restrict prices by advanced rules, such as a customer's group, zip code, or a cart's total.
-
-Each rule of a price is represented by the [PriceRule data model](https://docs.medusajs.com/references/pricing/models/PriceRule/index.html.md).
-
-The `Price` data model has a `rules_count` property, which indicates how many rules, represented by `PriceRule`, are applied to the price.
-
-For exmaple, you create a price restricted to `10557` zip codes.
-
-
-
-A price can have multiple price rules.
-
-For example, a price can be restricted by a region and a zip code.
-
-
-
-### Price List Rules
-
-Rules applied to a price list are represented by the [PriceListRule data model](https://docs.medusajs.com/references/pricing/models/PriceListRule/index.html.md).
-
-The `rules_count` property of a `PriceList` indicates how many rules are applied to it.
-
-
-
-### How to Create Prices with Rules?
-
-When you create prices, you can specify rules for each price. This allows you to create complex pricing strategies based on different contexts.
-
-For example:
-
-For most use cases where you're building customizations in the Medusa application, it's highly recommended to use [Medusa's workflows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/medusa-workflows-reference/index.html.md) rather than using the Pricing Module directly. Medusa's workflows already implement extensive functionalities that you can re-use in your custom flows, with reliable roll-back mechanism.
-
-### Using Medusa Workflows
-
-```ts highlights={workflowHighlights}
-const { result } = await createShippingOptionsWorkflow(container)
- .run({
- input: [{
- name: "Standard Shipping",
- service_zone_id: "serzo_123",
- shipping_profile_id: "sp_123",
- provider_id: "prov_123",
- type: {
- label: "Standard",
- description: "Standard shipping",
- code: "standard",
- },
- price_type: "flat",
- prices: [
- // default price
- {
- currency_code: "usd",
- amount: 10,
- rules: {},
- },
- // price if cart total >= $100
- {
- currency_code: "usd",
- amount: 0,
- rules: {
- item_total: {
- operator: "gte",
- value: 100,
- },
- },
- },
- ],
- }],
- })
-```
-
-### Using the Pricing Module
-
-```ts
-const priceSet = await pricingModule.addPrices({
- priceSetId: "pset_1",
- prices: [
- // default price
- {
- currency_code: "usd",
- amount: 10,
- rules: {},
- },
- // price if cart total >= $100
- {
- currency_code: "usd",
- amount: 0,
- rules: {
- item_total: {
- operator: "gte",
- value: 100,
- },
- },
- },
- ],
-})
-```
-
-In this example, you create a shipping option whose default price is `$10`. When the total of the cart or order using this shipping option is greater than `$100`, the shipping option's price becomes free.
-
-### How is the Price Rule Applied?
-
-The [price calculation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md) mechanism considers a price applicable when the resource that this price is in matches the specified rules.
-
-For example, a [cart object](https://docs.medusajs.com/api/store#carts_cart_schema) has an `item_total` property. So, if a shipping option has the following price:
-
-```json
-{
- "currency_code": "usd",
- "amount": 0,
- "rules": {
- "item_total": {
- "operator": "gte",
- "value": 100,
- }
- }
-}
-```
-
-The shipping option's price is applied when the cart's `item_total` is greater than or equal to `$100`.
-
-You can also apply the rule on nested relations and properties. For example, to apply a shipping option's price based on the customer's group, you can apply a rule on the `customer.group.id` attribute:
-
-```json
-{
- "currency_code": "usd",
- "amount": 0,
- "rules": {
- "customer.group.id": {
- "operator": "eq",
- "value": "cusgrp_123"
- }
- }
-}
-```
-
-In this example, the price is only applied if a cart's customer belongs to the customer group of ID `cusgrp_123`.
-
-These same rules apply to product variant prices as well, or any other resource that has a price.
-
-
-# Links between 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",
- },
-})
-```
-
-
-# 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
-
-
-# Links between Region Module and Other Modules
-
-This document showcases the module links defined between the Region Module and other Commerce Modules.
-
-## Summary
-
-The Region Module has the following links to other modules:
-
-Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database.
-
-|First Data Model|Second Data Model|Type|Description|
-|---|---|---|---|
-|Cart|Region|Read-only - has one|Learn more|
-|Order|Region|Read-only - has one|Learn more|
-|Region|PaymentProvider|Stored - many-to-many|Learn more|
-
-***
-
-## Cart Module
-
-Medusa defines a read-only link between the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `Cart` data model and the `Region` data model. Because the link is read-only from the `Cart`'s side, you can only retrieve the region of a cart, and not the other way around.
-
-### Retrieve with Query
-
-To retrieve the region of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `region.*` in `fields`:
-
-### query.graph
-
-```ts
-const { data: carts } = await query.graph({
- entity: "cart",
- fields: [
- "region.*",
- ],
-})
-
-// carts[0].region
-```
-
-### useQueryGraphStep
-
-```ts
-import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
-
-// ...
-
-const { data: carts } = useQueryGraphStep({
- entity: "cart",
- fields: [
- "region.*",
- ],
-})
-
-// carts[0].region
-```
-
-***
-
-## Order Module
-
-Medusa defines a read-only link between the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `Order` data model and the `Region` data model. Because the link is read-only from the `Order`'s side, you can only retrieve the region of an order, and not the other way around.
-
-### Retrieve with Query
-
-To retrieve the region of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `region.*` in `fields`:
-
-### query.graph
-
-```ts
-const { data: orders } = await query.graph({
- entity: "order",
- fields: [
- "region.*",
- ],
-})
-
-// orders[0].region
-```
-
-### useQueryGraphStep
-
-```ts
-import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
-
-// ...
-
-const { data: orders } = useQueryGraphStep({
- entity: "order",
- fields: [
- "region.*",
- ],
-})
-
-// orders[0].region
-```
-
-***
-
-## Payment Module
-
-You can specify for each region which payment providers are available for use.
-
-Medusa defines a module link between the `PaymentProvider` and the `Region` data models.
-
-
-
-### Retrieve with Query
-
-To retrieve the payment providers of a region with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `payment_providers.*` in `fields`:
-
-### query.graph
-
-```ts
-const { data: regions } = await query.graph({
- entity: "region",
- fields: [
- "payment_providers.*",
- ],
-})
-
-// regions[0].payment_providers
-```
-
-### useQueryGraphStep
-
-```ts
-import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
-
-// ...
-
-const { data: regions } = useQueryGraphStep({
- entity: "region",
- fields: [
- "payment_providers.*",
- ],
-})
-
-// regions[0].payment_providers
-```
-
-### Manage with Link
-
-To manage the payment providers in a region, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):
-
-### link.create
-
-```ts
-import { Modules } from "@medusajs/framework/utils"
-
-// ...
-
-await link.create({
- [Modules.REGION]: {
- region_id: "reg_123",
- },
- [Modules.PAYMENT]: {
- payment_provider_id: "pp_stripe_stripe",
- },
-})
-```
-
-### createRemoteLinkStep
-
-```ts
-import { Modules } from "@medusajs/framework/utils"
-import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"
-
-// ...
-
-createRemoteLinkStep({
- [Modules.REGION]: {
- region_id: "reg_123",
- },
- [Modules.PAYMENT]: {
- payment_provider_id: "pp_stripe_stripe",
- },
-})
-```
-
-
# Links between Product Module and Other Modules
This document showcases the module links defined between the Product Module and other Commerce Modules.
@@ -28471,71 +28449,385 @@ 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).
-# Stock Location Concepts
+# Promotion Actions
-In this document, you’ll learn about the main concepts in the Stock Location Module.
+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).
-## Stock Location
+## computeActions Method
-A stock location, represented by the `StockLocation` data model, represents a location where stock items are kept. For example, a warehouse.
+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.
-Medusa uses stock locations to provide inventory details, from the Inventory Module, per location.
+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.
***
-## StockLocationAddress
+## Action Types
-The `StockLocationAddress` data model belongs to the `StockLocation` data model. It provides more detailed information of the location, such as country code or street address.
+### `addItemAdjustment` Action
+
+The `addItemAdjustment` action indicates that an adjustment must be made to an item. For example, removing $5 off its amount.
+
+This action has the following format:
+
+```ts
+export interface AddItemAdjustmentAction {
+ action: "addItemAdjustment"
+ item_id: string
+ amount: number
+ code: string
+ description?: string
+}
+```
+
+This action means that a new record should be created of the `LineItemAdjustment` data model in the Cart Module, or `OrderLineItemAdjustment` data model in the Order Module.
+
+Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.AddItemAdjustmentAction/index.html.md) for details on the object’s properties.
+
+### `removeItemAdjustment` Action
+
+The `removeItemAdjustment` action indicates that an adjustment must be removed from a line item. For example, remove the $5 discount.
+
+The `computeActions` method accepts any previous item adjustments in the `items` property of the second parameter.
+
+This action has the following format:
+
+```ts
+export interface RemoveItemAdjustmentAction {
+ action: "removeItemAdjustment"
+ adjustment_id: string
+ description?: string
+ code: string
+}
+```
+
+This action means that a new record should be removed of the `LineItemAdjustment` (or `OrderLineItemAdjustment`) with the specified ID in the `adjustment_id` property.
+
+Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.RemoveItemAdjustmentAction/index.html.md) for details on the object’s properties.
+
+### `addShippingMethodAdjustment` Action
+
+The `addShippingMethodAdjustment` action indicates that an adjustment must be made on a shipping method. For example, make the shipping method free.
+
+This action has the following format:
+
+```ts
+export interface AddShippingMethodAdjustment {
+ action: "addShippingMethodAdjustment"
+ shipping_method_id: string
+ amount: number
+ code: string
+ description?: string
+}
+```
+
+This action means that a new record should be created of the `ShippingMethodAdjustment` data model in the Cart Module, or `OrderShippingMethodAdjustment` data model in the Order Module.
+
+Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.AddShippingMethodAdjustment/index.html.md) for details on the object’s properties.
+
+### `removeShippingMethodAdjustment` Action
+
+The `removeShippingMethodAdjustment` action indicates that an adjustment must be removed from a shipping method. For example, remove the free shipping discount.
+
+The `computeActions` method accepts any previous shipping method adjustments in the `shipping_methods` property of the second parameter.
+
+This action has the following format:
+
+```ts
+export interface RemoveShippingMethodAdjustment {
+ action: "removeShippingMethodAdjustment"
+ adjustment_id: string
+ code: string
+}
+```
+
+When the Medusa application receives this action type, it removes the `ShippingMethodAdjustment` (or `OrderShippingMethodAdjustment`) with the specified ID in the `adjustment_id` property.
+
+Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.RemoveShippingMethodAdjustment/index.html.md) for details on the object’s properties.
+
+### `campaignBudgetExceeded` Action
+
+When the `campaignBudgetExceeded` action is returned, the promotions within a campaign can no longer be used as the campaign budget has been exceeded.
+
+This action has the following format:
+
+```ts
+export interface CampaignBudgetExceededAction {
+ action: "campaignBudgetExceeded"
+ code: string
+}
+```
+
+Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.CampaignBudgetExceededAction/index.html.md) for details on the object’s properties.
-# Links between Stock Location Module and Other Modules
+# Campaign
-This document showcases the module links defined between the Stock Location Module and other Commerce Modules.
+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.
+
+
+
+
+# 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.
+
+
+# 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/resources/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/resources/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 Stock Location Module has the following links to other modules:
+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|
|---|---|---|---|
-|FulfillmentSet|StockLocation|Stored - many-to-one|Learn more|
-|FulfillmentProvider|StockLocation|Stored - many-to-many|Learn more|
-|InventoryLevel|StockLocation|Read-only - has many|Learn more|
-|SalesChannel|StockLocation|Stored - many-to-many|Learn more|
+|Cart|Promotion|Stored - many-to-many|Learn more|
+|LineItemAdjustment|Promotion|Read-only - has one|Learn more|
+|Order|Promotion|Stored - many-to-many|Learn more|
***
-## Fulfillment Module
+## Cart Module
-A fulfillment set can be conditioned to a specific stock location.
+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 defines a link between the `FulfillmentSet` and `StockLocation` data models.
+
-
-
-Medusa also defines a link between the `FulfillmentProvider` and `StockLocation` data models to indicate the providers that can be used in a location.
-
-
+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 fulfillment sets of a stock location with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `fulfillment_sets.*` in `fields`:
+To retrieve the 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 fulfillment providers, pass `fulfillment_providers.*` in `fields`.
+To retrieve the promotion of a line item adjustment, pass `promotion.*` in `fields`.
### query.graph
```ts
-const { data: stockLocations } = await query.graph({
- entity: "stock_location",
+const { data: promotions } = await query.graph({
+ entity: "promotion",
fields: [
- "fulfillment_sets.*",
+ "carts.*",
],
})
-// stockLocations[0].fulfillment_sets
+// promotions[0].carts
```
### useQueryGraphStep
@@ -28545,19 +28837,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
// ...
-const { data: stockLocations } = useQueryGraphStep({
- entity: "stock_location",
+const { data: promotions } = useQueryGraphStep({
+ entity: "promotion",
fields: [
- "fulfillment_sets.*",
+ "carts.*",
],
})
-// stockLocations[0].fulfillment_sets
+// promotions[0].carts
```
### 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):
+To manage the promotions of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):
### link.create
@@ -28567,11 +28859,11 @@ import { Modules } from "@medusajs/framework/utils"
// ...
await link.create({
- [Modules.STOCK_LOCATION]: {
- stock_location_id: "sloc_123",
+ [Modules.CART]: {
+ cart_id: "cart_123",
},
- [Modules.FULFILLMENT]: {
- fulfillment_set_id: "fset_123",
+ [Modules.PROMOTION]: {
+ promotion_id: "promo_123",
},
})
```
@@ -28585,36 +28877,38 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"
// ...
createRemoteLinkStep({
- [Modules.STOCK_LOCATION]: {
- stock_location_id: "sloc_123",
+ [Modules.CART]: {
+ cart_id: "cart_123",
},
- [Modules.FULFILLMENT]: {
- fulfillment_set_id: "fset_123",
+ [Modules.PROMOTION]: {
+ promotion_id: "promo_123",
},
})
```
***
-## Inventory Module
+## Order Module
-Medusa defines a read-only link between the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md)'s `InventoryLevel` data model and the `StockLocation` data model. Because the link is read-only from the `InventoryLevel`'s side, you can only retrieve the stock location of an inventory level, and not the other way around.
+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 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`:
+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: inventoryLevels } = await query.graph({
- entity: "inventory_level",
+const { data: promotions } = await query.graph({
+ entity: "promotion",
fields: [
- "stock_locations.*",
+ "orders.*",
],
})
-// inventoryLevels[0].stock_locations
+// promotions[0].orders
```
### useQueryGraphStep
@@ -28624,63 +28918,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
// ...
-const { data: inventoryLevels } = useQueryGraphStep({
- entity: "inventory_level",
+const { data: promotions } = useQueryGraphStep({
+ entity: "promotion",
fields: [
- "stock_locations.*",
+ "orders.*",
],
})
-// inventoryLevels[0].stock_locations
-```
-
-***
-
-## Sales Channel Module
-
-A stock location is associated with a sales channel. This scopes inventory quantities in a stock location by the associated sales channel.
-
-Medusa defines a link between the `SalesChannel` and `StockLocation` data models.
-
-
-
-### Retrieve with Query
-
-To retrieve the sales channels of a stock location with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channels.*` in `fields`:
-
-### query.graph
-
-```ts
-const { data: stockLocations } = await query.graph({
- entity: "stock_location",
- fields: [
- "sales_channels.*",
- ],
-})
-
-// stockLocations[0].sales_channels
-```
-
-### useQueryGraphStep
-
-```ts
-import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
-
-// ...
-
-const { data: stockLocations } = useQueryGraphStep({
- entity: "stock_location",
- fields: [
- "sales_channels.*",
- ],
-})
-
-// stockLocations[0].sales_channels
+// promotions[0].orders
```
### Manage with Link
-To manage the stock locations of a sales channel, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):
+To manage the promotion of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):
### link.create
@@ -28690,11 +28940,11 @@ import { Modules } from "@medusajs/framework/utils"
// ...
await link.create({
- [Modules.SALES_CHANNEL]: {
- sales_channel_id: "sc_123",
+ [Modules.ORDER]: {
+ order_id: "order_123",
},
- [Modules.STOCK_LOCATION]: {
- sales_channel_id: "sloc_123",
+ [Modules.PROMOTION]: {
+ promotion_id: "promo_123",
},
})
```
@@ -28708,11 +28958,11 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"
// ...
createRemoteLinkStep({
- [Modules.SALES_CHANNEL]: {
- sales_channel_id: "sc_123",
+ [Modules.ORDER]: {
+ order_id: "order_123",
},
- [Modules.STOCK_LOCATION]: {
- sales_channel_id: "sloc_123",
+ [Modules.PROMOTION]: {
+ promotion_id: "promo_123",
},
})
```
@@ -29121,6 +29371,433 @@ createRemoteLinkStep({
```
+# Links between Region Module and Other Modules
+
+This document showcases the module links defined between the Region Module and other Commerce Modules.
+
+## Summary
+
+The Region Module has the following links to other modules:
+
+Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database.
+
+|First Data Model|Second Data Model|Type|Description|
+|---|---|---|---|
+|Cart|Region|Read-only - has one|Learn more|
+|Order|Region|Read-only - has one|Learn more|
+|Region|PaymentProvider|Stored - many-to-many|Learn more|
+
+***
+
+## Cart Module
+
+Medusa defines a read-only link between the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `Cart` data model and the `Region` data model. Because the link is read-only from the `Cart`'s side, you can only retrieve the region of a cart, and not the other way around.
+
+### Retrieve with Query
+
+To retrieve the region of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `region.*` in `fields`:
+
+### query.graph
+
+```ts
+const { data: carts } = await query.graph({
+ entity: "cart",
+ fields: [
+ "region.*",
+ ],
+})
+
+// carts[0].region
+```
+
+### useQueryGraphStep
+
+```ts
+import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
+
+// ...
+
+const { data: carts } = useQueryGraphStep({
+ entity: "cart",
+ fields: [
+ "region.*",
+ ],
+})
+
+// carts[0].region
+```
+
+***
+
+## Order Module
+
+Medusa defines a read-only link between the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `Order` data model and the `Region` data model. Because the link is read-only from the `Order`'s side, you can only retrieve the region of an order, and not the other way around.
+
+### Retrieve with Query
+
+To retrieve the region of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `region.*` in `fields`:
+
+### query.graph
+
+```ts
+const { data: orders } = await query.graph({
+ entity: "order",
+ fields: [
+ "region.*",
+ ],
+})
+
+// orders[0].region
+```
+
+### useQueryGraphStep
+
+```ts
+import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
+
+// ...
+
+const { data: orders } = useQueryGraphStep({
+ entity: "order",
+ fields: [
+ "region.*",
+ ],
+})
+
+// orders[0].region
+```
+
+***
+
+## Payment Module
+
+You can specify for each region which payment providers are available for use.
+
+Medusa defines a module link between the `PaymentProvider` and the `Region` data models.
+
+
+
+### Retrieve with Query
+
+To retrieve the payment providers of a region with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `payment_providers.*` in `fields`:
+
+### query.graph
+
+```ts
+const { data: regions } = await query.graph({
+ entity: "region",
+ fields: [
+ "payment_providers.*",
+ ],
+})
+
+// regions[0].payment_providers
+```
+
+### useQueryGraphStep
+
+```ts
+import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
+
+// ...
+
+const { data: regions } = useQueryGraphStep({
+ entity: "region",
+ fields: [
+ "payment_providers.*",
+ ],
+})
+
+// regions[0].payment_providers
+```
+
+### Manage with Link
+
+To manage the payment providers in a region, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):
+
+### link.create
+
+```ts
+import { Modules } from "@medusajs/framework/utils"
+
+// ...
+
+await link.create({
+ [Modules.REGION]: {
+ region_id: "reg_123",
+ },
+ [Modules.PAYMENT]: {
+ payment_provider_id: "pp_stripe_stripe",
+ },
+})
+```
+
+### createRemoteLinkStep
+
+```ts
+import { Modules } from "@medusajs/framework/utils"
+import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"
+
+// ...
+
+createRemoteLinkStep({
+ [Modules.REGION]: {
+ region_id: "reg_123",
+ },
+ [Modules.PAYMENT]: {
+ payment_provider_id: "pp_stripe_stripe",
+ },
+})
+```
+
+
+# Stock Location Concepts
+
+In this document, you’ll learn about the main concepts in the Stock Location Module.
+
+## Stock Location
+
+A stock location, represented by the `StockLocation` data model, represents a location where stock items are kept. For example, a warehouse.
+
+Medusa uses stock locations to provide inventory details, from the Inventory Module, per location.
+
+***
+
+## StockLocationAddress
+
+The `StockLocationAddress` data model belongs to the `StockLocation` data model. It provides more detailed information of the location, such as country code or street address.
+
+
+# Links between Stock Location Module and Other Modules
+
+This document showcases the module links defined between the Stock Location Module and other Commerce Modules.
+
+## Summary
+
+The Stock Location Module has the following links to other modules:
+
+Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database.
+
+|First Data Model|Second Data Model|Type|Description|
+|---|---|---|---|
+|FulfillmentSet|StockLocation|Stored - many-to-one|Learn more|
+|FulfillmentProvider|StockLocation|Stored - many-to-many|Learn more|
+|InventoryLevel|StockLocation|Read-only - has many|Learn more|
+|SalesChannel|StockLocation|Stored - many-to-many|Learn more|
+
+***
+
+## Fulfillment Module
+
+A fulfillment set can be conditioned to a specific stock location.
+
+Medusa defines a link between the `FulfillmentSet` and `StockLocation` data models.
+
+
+
+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 fulfillment sets of a stock location with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `fulfillment_sets.*` in `fields`:
+
+To retrieve the fulfillment providers, pass `fulfillment_providers.*` in `fields`.
+
+### query.graph
+
+```ts
+const { data: stockLocations } = await query.graph({
+ entity: "stock_location",
+ fields: [
+ "fulfillment_sets.*",
+ ],
+})
+
+// stockLocations[0].fulfillment_sets
+```
+
+### useQueryGraphStep
+
+```ts
+import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
+
+// ...
+
+const { data: stockLocations } = useQueryGraphStep({
+ entity: "stock_location",
+ fields: [
+ "fulfillment_sets.*",
+ ],
+})
+
+// stockLocations[0].fulfillment_sets
+```
+
+### Manage with Link
+
+To manage the stock location of a fulfillment set, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):
+
+### link.create
+
+```ts
+import { Modules } from "@medusajs/framework/utils"
+
+// ...
+
+await link.create({
+ [Modules.STOCK_LOCATION]: {
+ stock_location_id: "sloc_123",
+ },
+ [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",
+ },
+})
+```
+
+***
+
+## Inventory Module
+
+Medusa defines a read-only link between the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md)'s `InventoryLevel` data model and the `StockLocation` data model. Because the link is read-only from the `InventoryLevel`'s side, you can only retrieve the stock location of an inventory level, and not the other way around.
+
+### Retrieve with Query
+
+To retrieve the stock locations of an inventory level with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `stock_locations.*` in `fields`:
+
+### query.graph
+
+```ts
+const { data: inventoryLevels } = await query.graph({
+ entity: "inventory_level",
+ fields: [
+ "stock_locations.*",
+ ],
+})
+
+// inventoryLevels[0].stock_locations
+```
+
+### useQueryGraphStep
+
+```ts
+import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
+
+// ...
+
+const { data: inventoryLevels } = useQueryGraphStep({
+ entity: "inventory_level",
+ fields: [
+ "stock_locations.*",
+ ],
+})
+
+// inventoryLevels[0].stock_locations
+```
+
+***
+
+## Sales Channel Module
+
+A stock location is associated with a sales channel. This scopes inventory quantities in a stock location by the associated sales channel.
+
+Medusa defines a link between the `SalesChannel` and `StockLocation` data models.
+
+
+
+### Retrieve with Query
+
+To retrieve the sales channels of a stock location with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channels.*` in `fields`:
+
+### query.graph
+
+```ts
+const { data: stockLocations } = await query.graph({
+ entity: "stock_location",
+ fields: [
+ "sales_channels.*",
+ ],
+})
+
+// stockLocations[0].sales_channels
+```
+
+### useQueryGraphStep
+
+```ts
+import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
+
+// ...
+
+const { data: stockLocations } = useQueryGraphStep({
+ entity: "stock_location",
+ fields: [
+ "sales_channels.*",
+ ],
+})
+
+// stockLocations[0].sales_channels
+```
+
+### Manage with Link
+
+To manage the stock locations of a sales channel, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):
+
+### link.create
+
+```ts
+import { Modules } from "@medusajs/framework/utils"
+
+// ...
+
+await link.create({
+ [Modules.SALES_CHANNEL]: {
+ sales_channel_id: "sc_123",
+ },
+ [Modules.STOCK_LOCATION]: {
+ sales_channel_id: "sloc_123",
+ },
+})
+```
+
+### createRemoteLinkStep
+
+```ts
+import { Modules } from "@medusajs/framework/utils"
+import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"
+
+// ...
+
+createRemoteLinkStep({
+ [Modules.SALES_CHANNEL]: {
+ sales_channel_id: "sc_123",
+ },
+ [Modules.STOCK_LOCATION]: {
+ sales_channel_id: "sloc_123",
+ },
+})
+```
+
+
# Links between Store Module and Other Modules
This document showcases the module links defined between the Store Module and other Commerce Modules.
@@ -29178,43 +29855,6 @@ const { data: stores } = useQueryGraphStep({
```
-# User Module Options
-
-In this document, you'll learn about the options of the User Module.
-
-## Module Options
-
-```ts title="medusa-config.ts"
-import { Modules } from "@medusajs/framework/utils"
-
-// ...
-
-module.exports = defineConfig({
- // ...
- modules: [
- {
- resolve: "@medusajs/user",
- options: {
- jwt_secret: process.env.JWT_SECRET,
- },
- },
- ],
-})
-```
-
-|Option|Description|Required|
-|---|---|---|---|---|
-|\`jwt\_secret\`|A string indicating the secret used to sign the invite tokens.|Yes|
-
-### Environment Variables
-
-Make sure to add the necessary environment variables for the above options in `.env`:
-
-```bash
-JWT_SECRET=supersecret
-```
-
-
# User Creation Flows
In this document, learn the different ways to create a user using the User Module.
@@ -29295,6 +29935,112 @@ if (!count) {
```
+# User Module Options
+
+In this document, you'll learn about the options of the User Module.
+
+## Module Options
+
+```ts title="medusa-config.ts"
+import { Modules } from "@medusajs/framework/utils"
+
+// ...
+
+module.exports = defineConfig({
+ // ...
+ modules: [
+ {
+ resolve: "@medusajs/user",
+ options: {
+ jwt_secret: process.env.JWT_SECRET,
+ },
+ },
+ ],
+})
+```
+
+|Option|Description|Required|
+|---|---|---|---|---|
+|\`jwt\_secret\`|A string indicating the secret used to sign the invite tokens.|Yes|
+
+### Environment Variables
+
+Make sure to add the necessary environment variables for the above options in `.env`:
+
+```bash
+JWT_SECRET=supersecret
+```
+
+
+# Tax Calculation with the Tax Provider
+
+In this guide, you’ll learn how tax lines are calculated using the tax provider.
+
+## Tax Lines Calculation
+
+Tax lines are calculated and retrieved using the [getTaxLines method of the Tax Module’s main service](https://docs.medusajs.com/references/tax/getTaxLines/index.html.md). It accepts an array of line items and shipping methods, and the context of the calculation.
+
+For example:
+
+```ts
+const taxLines = await taxModuleService.getTaxLines(
+ [
+ {
+ id: "cali_123",
+ product_id: "prod_123",
+ unit_price: 1000,
+ quantity: 1,
+ },
+ {
+ id: "casm_123",
+ shipping_option_id: "so_123",
+ unit_price: 2000,
+ },
+ ],
+ {
+ address: {
+ country_code: "us",
+ },
+ }
+)
+```
+
+The context object is used to determine which tax regions and rates to use in the calculation. It includes properties related to the address and customer.
+
+The example above retrieves the tax lines based on the tax region for the United States.
+
+The method returns tax lines for the line item and shipping methods. For example:
+
+```json
+[
+ {
+ "line_item_id": "cali_123",
+ "rate_id": "txr_1",
+ "rate": 10,
+ "code": "XXX",
+ "name": "Tax Rate 1"
+ },
+ {
+ "shipping_line_id": "casm_123",
+ "rate_id": "txr_2",
+ "rate": 5,
+ "code": "YYY",
+ "name": "Tax Rate 2"
+ }
+]
+```
+
+***
+
+## Using the Tax Provider in the Calculation
+
+The tax lines retrieved by the `getTaxLines` method are actually retrieved from the tax region’s [Tax Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-provider/index.html.md).
+
+A tax module implements the logic to shape tax lines. Each tax region uses a tax provider.
+
+Learn more about tax providers, configuring, and creating them in the [Tax Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-provider/index.html.md) guide.
+
+
# Tax Module Options
In this guide, you'll learn about the options of the Tax Module.
@@ -29338,6 +30084,79 @@ The objects in the array accept the following properties:
- `options`: An optional object of the module provider's options.
+# Tax Rates and Rules
+
+In this document, you’ll learn about tax rates and rules.
+
+Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions#manage-tax-rate-overrides/index.html.md) to learn how to manage tax rates using the dashboard.
+
+## What are Tax Rates?
+
+A tax rate is a percentage amount used to calculate the tax amount for each taxable item’s price, such as line items or shipping methods, in a cart. The sum of all calculated tax amounts are then added to the cart’s total as a tax total.
+
+Each tax region has a default tax rate. This tax rate is applied to all taxable items of a cart in that region.
+
+### Combinable Tax Rates
+
+Tax regions can have parent tax regions. To inherit the tax rates of the parent tax region, set the `is_combinable` of the child’s tax rates to `true`.
+
+Then, when tax rates are retrieved for a taxable item in the child region, both the child and the parent tax regions’ applicable rates are returned.
+
+***
+
+## Override Tax Rates with Rules
+
+You can create tax rates that override the default for specific conditions or rules.
+
+For example, you can have a default tax rate is 10%, but for products of type “Shirt” is %15.
+
+A tax region can have multiple tax rates, and each tax rate can have multiple tax rules. The [TaxRateRule data model](https://docs.medusajs.com/references/tax/models/TaxRateRule/index.html.md) represents a tax rate’s rule.
+
+
+
+These two properties of the data model identify the rule’s target:
+
+- `reference`: the name of the table in the database that this rule points to. For example, `product_type`.
+- `reference_id`: the ID of the data model’s record that this points to. For example, a product type’s ID.
+
+So, to override the default tax rate for product types “Shirt”, you create a tax rate and associate with it a tax rule whose `reference` is `product_type` and `reference_id` the ID of the “Shirt” product type.
+
+
+# Tax Region
+
+In this document, you’ll learn about tax regions and how to use them with the Region Module.
+
+Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md) to learn how to manage tax regions using the dashboard.
+
+## What is a Tax Region?
+
+A tax region, represented by the [TaxRegion data model](https://docs.medusajs.com/references/tax/models/TaxRegion/index.html.md), stores tax settings related to a region that your store serves.
+
+Tax regions can inherit settings and rules from a parent tax region.
+
+***
+
+## Tax Rules in a Tax Region
+
+Tax rules define the tax rates and behavior within a tax region. They specify:
+
+- The tax rate percentage.
+- Which products the tax applies to.
+- Other custom rules to determine tax applicability.
+
+Learn more about tax rules in the [Tax Rates and Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-rates-and-rules/index.html.md) guide.
+
+***
+
+## Tax Provider
+
+Each tax region can have a default tax provider. The tax provider is responsible for calculating the tax lines for carts and orders in that region.
+
+You can use Medusa's default tax provider or create a custom one, allowing you to integrate with third-party tax services or implement your own tax calculation logic.
+
+Learn more about tax providers in the [Tax Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-provider/index.html.md) guide.
+
+
# Tax Module Provider
In this guide, you’ll learn about the Tax Module Provider and how it's used.
@@ -29412,667 +30231,6 @@ You can remove a registered tax provider from the Medusa application by removing
Then, the next time the Medusa application starts, it will set the `is_enabled` property of the `TaxProvider`'s record to `false`. This allows you to re-enable the tax provider later if needed by adding it back to the `providers` option.
-# Tax Calculation with the Tax Provider
-
-In this guide, you’ll learn how tax lines are calculated using the tax provider.
-
-## Tax Lines Calculation
-
-Tax lines are calculated and retrieved using the [getTaxLines method of the Tax Module’s main service](https://docs.medusajs.com/references/tax/getTaxLines/index.html.md). It accepts an array of line items and shipping methods, and the context of the calculation.
-
-For example:
-
-```ts
-const taxLines = await taxModuleService.getTaxLines(
- [
- {
- id: "cali_123",
- product_id: "prod_123",
- unit_price: 1000,
- quantity: 1,
- },
- {
- id: "casm_123",
- shipping_option_id: "so_123",
- unit_price: 2000,
- },
- ],
- {
- address: {
- country_code: "us",
- },
- }
-)
-```
-
-The context object is used to determine which tax regions and rates to use in the calculation. It includes properties related to the address and customer.
-
-The example above retrieves the tax lines based on the tax region for the United States.
-
-The method returns tax lines for the line item and shipping methods. For example:
-
-```json
-[
- {
- "line_item_id": "cali_123",
- "rate_id": "txr_1",
- "rate": 10,
- "code": "XXX",
- "name": "Tax Rate 1"
- },
- {
- "shipping_line_id": "casm_123",
- "rate_id": "txr_2",
- "rate": 5,
- "code": "YYY",
- "name": "Tax Rate 2"
- }
-]
-```
-
-***
-
-## Using the Tax Provider in the Calculation
-
-The tax lines retrieved by the `getTaxLines` method are actually retrieved from the tax region’s [Tax Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-provider/index.html.md).
-
-A tax module implements the logic to shape tax lines. Each tax region uses a tax provider.
-
-Learn more about tax providers, configuring, and creating them in the [Tax Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-provider/index.html.md) guide.
-
-
-# Tax Rates and Rules
-
-In this document, you’ll learn about tax rates and rules.
-
-Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions#manage-tax-rate-overrides/index.html.md) to learn how to manage tax rates using the dashboard.
-
-## What are Tax Rates?
-
-A tax rate is a percentage amount used to calculate the tax amount for each taxable item’s price, such as line items or shipping methods, in a cart. The sum of all calculated tax amounts are then added to the cart’s total as a tax total.
-
-Each tax region has a default tax rate. This tax rate is applied to all taxable items of a cart in that region.
-
-### Combinable Tax Rates
-
-Tax regions can have parent tax regions. To inherit the tax rates of the parent tax region, set the `is_combinable` of the child’s tax rates to `true`.
-
-Then, when tax rates are retrieved for a taxable item in the child region, both the child and the parent tax regions’ applicable rates are returned.
-
-***
-
-## Override Tax Rates with Rules
-
-You can create tax rates that override the default for specific conditions or rules.
-
-For example, you can have a default tax rate is 10%, but for products of type “Shirt” is %15.
-
-A tax region can have multiple tax rates, and each tax rate can have multiple tax rules. The [TaxRateRule data model](https://docs.medusajs.com/references/tax/models/TaxRateRule/index.html.md) represents a tax rate’s rule.
-
-
-
-These two properties of the data model identify the rule’s target:
-
-- `reference`: the name of the table in the database that this rule points to. For example, `product_type`.
-- `reference_id`: the ID of the data model’s record that this points to. For example, a product type’s ID.
-
-So, to override the default tax rate for product types “Shirt”, you create a tax rate and associate with it a tax rule whose `reference` is `product_type` and `reference_id` the ID of the “Shirt” product type.
-
-
-# Promotion Actions
-
-In this document, you’ll learn about promotion actions and how they’re computed using the [computeActions method](https://docs.medusajs.com/references/promotion/computeActions/index.html.md).
-
-## computeActions Method
-
-The Promotion Module's main service has a [computeActions method](https://docs.medusajs.com/references/promotion/computeActions/index.html.md) that returns an array of actions to perform on a cart when one or more promotions are applied.
-
-Actions inform you what adjustment must be made to a cart item or shipping method. Each action is an object having the `action` property indicating the type of action.
-
-***
-
-## Action Types
-
-### `addItemAdjustment` Action
-
-The `addItemAdjustment` action indicates that an adjustment must be made to an item. For example, removing $5 off its amount.
-
-This action has the following format:
-
-```ts
-export interface AddItemAdjustmentAction {
- action: "addItemAdjustment"
- item_id: string
- amount: number
- code: string
- description?: string
-}
-```
-
-This action means that a new record should be created of the `LineItemAdjustment` data model in the Cart Module, or `OrderLineItemAdjustment` data model in the Order Module.
-
-Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.AddItemAdjustmentAction/index.html.md) for details on the object’s properties.
-
-### `removeItemAdjustment` Action
-
-The `removeItemAdjustment` action indicates that an adjustment must be removed from a line item. For example, remove the $5 discount.
-
-The `computeActions` method accepts any previous item adjustments in the `items` property of the second parameter.
-
-This action has the following format:
-
-```ts
-export interface RemoveItemAdjustmentAction {
- action: "removeItemAdjustment"
- adjustment_id: string
- description?: string
- code: string
-}
-```
-
-This action means that a new record should be removed of the `LineItemAdjustment` (or `OrderLineItemAdjustment`) with the specified ID in the `adjustment_id` property.
-
-Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.RemoveItemAdjustmentAction/index.html.md) for details on the object’s properties.
-
-### `addShippingMethodAdjustment` Action
-
-The `addShippingMethodAdjustment` action indicates that an adjustment must be made on a shipping method. For example, make the shipping method free.
-
-This action has the following format:
-
-```ts
-export interface AddShippingMethodAdjustment {
- action: "addShippingMethodAdjustment"
- shipping_method_id: string
- amount: number
- code: string
- description?: string
-}
-```
-
-This action means that a new record should be created of the `ShippingMethodAdjustment` data model in the Cart Module, or `OrderShippingMethodAdjustment` data model in the Order Module.
-
-Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.AddShippingMethodAdjustment/index.html.md) for details on the object’s properties.
-
-### `removeShippingMethodAdjustment` Action
-
-The `removeShippingMethodAdjustment` action indicates that an adjustment must be removed from a shipping method. For example, remove the free shipping discount.
-
-The `computeActions` method accepts any previous shipping method adjustments in the `shipping_methods` property of the second parameter.
-
-This action has the following format:
-
-```ts
-export interface RemoveShippingMethodAdjustment {
- action: "removeShippingMethodAdjustment"
- adjustment_id: string
- code: string
-}
-```
-
-When the Medusa application receives this action type, it removes the `ShippingMethodAdjustment` (or `OrderShippingMethodAdjustment`) with the specified ID in the `adjustment_id` property.
-
-Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.RemoveShippingMethodAdjustment/index.html.md) for details on the object’s properties.
-
-### `campaignBudgetExceeded` Action
-
-When the `campaignBudgetExceeded` action is returned, the promotions within a campaign can no longer be used as the campaign budget has been exceeded.
-
-This action has the following format:
-
-```ts
-export interface CampaignBudgetExceededAction {
- action: "campaignBudgetExceeded"
- code: string
-}
-```
-
-Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.CampaignBudgetExceededAction/index.html.md) for details on the object’s properties.
-
-
-# Tax Region
-
-In this document, you’ll learn about tax regions and how to use them with the Region Module.
-
-Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md) to learn how to manage tax regions using the dashboard.
-
-## What is a Tax Region?
-
-A tax region, represented by the [TaxRegion data model](https://docs.medusajs.com/references/tax/models/TaxRegion/index.html.md), stores tax settings related to a region that your store serves.
-
-Tax regions can inherit settings and rules from a parent tax region.
-
-***
-
-## Tax Rules in a Tax Region
-
-Tax rules define the tax rates and behavior within a tax region. They specify:
-
-- The tax rate percentage.
-- Which products the tax applies to.
-- Other custom rules to determine tax applicability.
-
-Learn more about tax rules in the [Tax Rates and Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-rates-and-rules/index.html.md) guide.
-
-***
-
-## Tax Provider
-
-Each tax region can have a default tax provider. The tax provider is responsible for calculating the tax lines for carts and orders in that region.
-
-You can use Medusa's default tax provider or create a custom one, allowing you to integrate with third-party tax services or implement your own tax calculation logic.
-
-Learn more about tax providers in the [Tax Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-provider/index.html.md) guide.
-
-
-# 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.
-
-
-# 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/resources/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/resources/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`.
-
-
-# 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.
-
-
-
-
-# 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",
- },
-})
-```
-
-
# Emailpass Auth Module Provider
In this document, you’ll learn about the Emailpass auth module provider and how to install and use it in the Auth Module.
@@ -30304,86 +30462,6 @@ The [Authenticate or Login API Route](https://docs.medusajs.com/Users/shahednass
- [How to implement Google social login in the storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/third-party-login/index.html.md).
-# Get Product Variant Prices using Query
-
-In this document, you'll learn how to retrieve product variant prices in the Medusa application using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md).
-
-The Product Module doesn't provide pricing functionalities. The Medusa application links the Product Module's `ProductVariant` data model to the Pricing Module's `PriceSet` data model.
-
-So, to retrieve data across the linked records of the two modules, you use Query.
-
-## Retrieve All Product Variant Prices
-
-To retrieve all product variant prices, retrieve the product using Query and include among its fields `variants.prices.*`.
-
-For example:
-
-```ts highlights={[["6"]]}
-const { data: products } = await query.graph({
- entity: "product",
- fields: [
- "*",
- "variants.*",
- "variants.prices.*",
- ],
- filters: {
- id: [
- "prod_123",
- ],
- },
-})
-```
-
-Each variant in the retrieved products has a `prices` array property with all the product variant prices. Each price object has the properties of the [Pricing Module's Price data model](https://docs.medusajs.com/references/pricing/models/Price/index.html.md).
-
-***
-
-## Retrieve Calculated Price for a Context
-
-The Pricing Module can calculate prices of a variant based on a [context](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md), such as the region ID or the currency code.
-
-Learn more about prices calculation in [this Pricing Module documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md).
-
-To retrieve calculated prices of variants based on a context, retrieve the products using Query and:
-
-- Pass `variants.calculated_price.*` in the `fields` property.
-- Pass a `context` property in the object parameter. Its value is an object of objects that sets the context for the retrieved fields.
-
-For example:
-
-```ts highlights={[["10"], ["15"], ["16"], ["17"], ["18"], ["19"], ["20"], ["21"], ["22"]]}
-import { QueryContext } from "@medusajs/framework/utils"
-
-// ...
-
-const { data: products } = await query.graph({
- entity: "product",
- fields: [
- "*",
- "variants.*",
- "variants.calculated_price.*",
- ],
- filters: {
- id: "prod_123",
- },
- context: {
- variants: {
- calculated_price: QueryContext({
- region_id: "reg_01J3MRPDNXXXDSCC76Y6YCZARS",
- currency_code: "eur",
- }),
- },
- },
-})
-```
-
-For the context of the product variant's calculated price, you pass an object to `context` with the property `variants`, whose value is another object with the property `calculated_price`.
-
-`calculated_price`'s value is created using `QueryContext` from the Modules SDK, passing it a [calculation context object](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md).
-
-Each variant in the retrieved products has a `calculated_price` object. Learn more about its properties in [this Pricing Module guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#returned-price-object/index.html.md).
-
-
# Calculate Product Variant Price with Taxes
In this document, you'll learn how to calculate a product variant's price with taxes.
@@ -30699,6 +30777,86 @@ When you set up the webhook in Stripe, choose the following events to listen to:
- [Add Saved Payment Methods with Stripe](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/how-to-tutorials/tutorials/saved-payment-methods/index.html.md).
+# Get Product Variant Prices using Query
+
+In this document, you'll learn how to retrieve product variant prices in the Medusa application using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md).
+
+The Product Module doesn't provide pricing functionalities. The Medusa application links the Product Module's `ProductVariant` data model to the Pricing Module's `PriceSet` data model.
+
+So, to retrieve data across the linked records of the two modules, you use Query.
+
+## Retrieve All Product Variant Prices
+
+To retrieve all product variant prices, retrieve the product using Query and include among its fields `variants.prices.*`.
+
+For example:
+
+```ts highlights={[["6"]]}
+const { data: products } = await query.graph({
+ entity: "product",
+ fields: [
+ "*",
+ "variants.*",
+ "variants.prices.*",
+ ],
+ filters: {
+ id: [
+ "prod_123",
+ ],
+ },
+})
+```
+
+Each variant in the retrieved products has a `prices` array property with all the product variant prices. Each price object has the properties of the [Pricing Module's Price data model](https://docs.medusajs.com/references/pricing/models/Price/index.html.md).
+
+***
+
+## Retrieve Calculated Price for a Context
+
+The Pricing Module can calculate prices of a variant based on a [context](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md), such as the region ID or the currency code.
+
+Learn more about prices calculation in [this Pricing Module documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md).
+
+To retrieve calculated prices of variants based on a context, retrieve the products using Query and:
+
+- Pass `variants.calculated_price.*` in the `fields` property.
+- Pass a `context` property in the object parameter. Its value is an object of objects that sets the context for the retrieved fields.
+
+For example:
+
+```ts highlights={[["10"], ["15"], ["16"], ["17"], ["18"], ["19"], ["20"], ["21"], ["22"]]}
+import { QueryContext } from "@medusajs/framework/utils"
+
+// ...
+
+const { data: products } = await query.graph({
+ entity: "product",
+ fields: [
+ "*",
+ "variants.*",
+ "variants.calculated_price.*",
+ ],
+ filters: {
+ id: "prod_123",
+ },
+ context: {
+ variants: {
+ calculated_price: QueryContext({
+ region_id: "reg_01J3MRPDNXXXDSCC76Y6YCZARS",
+ currency_code: "eur",
+ }),
+ },
+ },
+})
+```
+
+For the context of the product variant's calculated price, you pass an object to `context` with the property `variants`, whose value is another object with the property `calculated_price`.
+
+`calculated_price`'s value is created using `QueryContext` from the Modules SDK, passing it a [calculation context object](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md).
+
+Each variant in the retrieved products has a `calculated_price` object. Learn more about its properties in [this Pricing Module guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#returned-price-object/index.html.md).
+
+
# 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).
@@ -30855,137 +31013,297 @@ Then, you pass the first sales channel ID to the `getVariantAvailability` functi
## 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)
- [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)
+- [generateResetPasswordTokenWorkflow](https://docs.medusajs.com/references/medusa-workflows/generateResetPasswordTokenWorkflow/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)
- [removeCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeCustomerAccountWorkflow/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)
-- [generateResetPasswordTokenWorkflow](https://docs.medusajs.com/references/medusa-workflows/generateResetPasswordTokenWorkflow/index.html.md)
-- [createCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerGroupsWorkflow/index.html.md)
-- [deleteCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomerGroupsWorkflow/index.html.md)
-- [linkCustomerGroupsToCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkCustomerGroupsToCustomerWorkflow/index.html.md)
-- [updateCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerGroupsWorkflow/index.html.md)
- [batchLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinksWorkflow/index.html.md)
- [createLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/createLinksWorkflow/index.html.md)
- [dismissLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/dismissLinksWorkflow/index.html.md)
- [updateLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateLinksWorkflow/index.html.md)
-- [createDefaultsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createDefaultsWorkflow/index.html.md)
-- [linkCustomersToCustomerGroupWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkCustomersToCustomerGroupWorkflow/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)
-- [beginDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginDraftOrderEditWorkflow/index.html.md)
-- [cancelDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelDraftOrderEditWorkflow/index.html.md)
-- [addDraftOrderItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addDraftOrderItemsWorkflow/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)
-- [removeDraftOrderActionShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderActionShippingMethodWorkflow/index.html.md)
-- [removeDraftOrderActionItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderActionItemWorkflow/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)
-- [updateDraftOrderActionItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderActionItemWorkflow/index.html.md)
-- [removeDraftOrderShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderShippingMethodWorkflow/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)
-- [confirmDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmDraftOrderEditWorkflow/index.html.md)
-- [updateDraftOrderStep](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderStep/index.html.md)
-- [updateDraftOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderWorkflow/index.html.md)
-- [deleteFilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteFilesWorkflow/index.html.md)
-- [uploadFilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/uploadFilesWorkflow/index.html.md)
-- [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)
-- [createInventoryItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInventoryItemsWorkflow/index.html.md)
-- [deleteInventoryItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInventoryItemWorkflow/index.html.md)
-- [updateInventoryItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateInventoryItemsWorkflow/index.html.md)
-- [updateInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateInventoryLevelsWorkflow/index.html.md)
-- [validateInventoryLevelsDelete](https://docs.medusajs.com/references/medusa-workflows/validateInventoryLevelsDelete/index.html.md)
-- [deleteInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInventoryLevelsWorkflow/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)
+- [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)
+- [createCartCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartCreditLinesWorkflow/index.html.md)
- [createPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentCollectionForCartWorkflow/index.html.md)
- [deleteCartCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCartCreditLinesWorkflow/index.html.md)
- [listShippingOptionsForCartWithPricingWorkflow](https://docs.medusajs.com/references/medusa-workflows/listShippingOptionsForCartWithPricingWorkflow/index.html.md)
- [listShippingOptionsForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/listShippingOptionsForCartWorkflow/index.html.md)
+- [refreshCartItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartItemsWorkflow/index.html.md)
- [refreshCartShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartShippingMethodsWorkflow/index.html.md)
- [refreshPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshPaymentCollectionForCartWorkflow/index.html.md)
- [refundPaymentAndRecreatePaymentSessionWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentAndRecreatePaymentSessionWorkflow/index.html.md)
- [transferCartCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/transferCartCustomerWorkflow/index.html.md)
- [updateCartPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartPromotionsWorkflow/index.html.md)
+- [updateLineItemInCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateLineItemInCartWorkflow/index.html.md)
- [updateCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartWorkflow/index.html.md)
- [updateTaxLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxLinesWorkflow/index.html.md)
- [validateExistingPaymentCollectionStep](https://docs.medusajs.com/references/medusa-workflows/validateExistingPaymentCollectionStep/index.html.md)
-- [updateLineItemInCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateLineItemInCartWorkflow/index.html.md)
-- [refreshCartItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartItemsWorkflow/index.html.md)
-- [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md)
-- [batchShippingOptionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchShippingOptionRulesWorkflow/index.html.md)
+- [linkCustomersToCustomerGroupWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkCustomersToCustomerGroupWorkflow/index.html.md)
+- [linkCustomerGroupsToCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkCustomerGroupsToCustomerWorkflow/index.html.md)
+- [createCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerGroupsWorkflow/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)
+- [createDefaultsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createDefaultsWorkflow/index.html.md)
+- [addDraftOrderItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addDraftOrderItemsWorkflow/index.html.md)
+- [addDraftOrderShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addDraftOrderShippingMethodsWorkflow/index.html.md)
+- [beginDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginDraftOrderEditWorkflow/index.html.md)
+- [addDraftOrderPromotionWorkflow](https://docs.medusajs.com/references/medusa-workflows/addDraftOrderPromotionWorkflow/index.html.md)
+- [convertDraftOrderStep](https://docs.medusajs.com/references/medusa-workflows/convertDraftOrderStep/index.html.md)
+- [removeDraftOrderActionItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderActionItemWorkflow/index.html.md)
+- [cancelDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelDraftOrderEditWorkflow/index.html.md)
+- [confirmDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmDraftOrderEditWorkflow/index.html.md)
+- [convertDraftOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/convertDraftOrderWorkflow/index.html.md)
+- [removeDraftOrderActionShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderActionShippingMethodWorkflow/index.html.md)
+- [updateDraftOrderActionItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderActionItemWorkflow/index.html.md)
+- [requestDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestDraftOrderEditWorkflow/index.html.md)
+- [removeDraftOrderShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderShippingMethodWorkflow/index.html.md)
+- [updateDraftOrderActionShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderActionShippingMethodWorkflow/index.html.md)
+- [removeDraftOrderPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderPromotionsWorkflow/index.html.md)
+- [updateDraftOrderShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderShippingMethodWorkflow/index.html.md)
+- [updateDraftOrderItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderItemWorkflow/index.html.md)
+- [updateDraftOrderStep](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderStep/index.html.md)
+- [deleteFilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteFilesWorkflow/index.html.md)
+- [uploadFilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/uploadFilesWorkflow/index.html.md)
+- [updateDraftOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderWorkflow/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)
- [createShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShipmentWorkflow/index.html.md)
- [createShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShippingOptionsWorkflow/index.html.md)
-- [createShippingProfilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShippingProfilesWorkflow/index.html.md)
- [deleteFulfillmentSetsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteFulfillmentSetsWorkflow/index.html.md)
- [deleteServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteServiceZonesWorkflow/index.html.md)
+- [createShippingProfilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShippingProfilesWorkflow/index.html.md)
- [deleteShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteShippingOptionsWorkflow/index.html.md)
+- [updateServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateServiceZonesWorkflow/index.html.md)
- [markFulfillmentAsDeliveredWorkflow](https://docs.medusajs.com/references/medusa-workflows/markFulfillmentAsDeliveredWorkflow/index.html.md)
- [updateFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateFulfillmentWorkflow/index.html.md)
-- [updateServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateServiceZonesWorkflow/index.html.md)
- [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)
-- [calculateShippingOptionsPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/calculateShippingOptionsPricesWorkflow/index.html.md)
-- [createServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createServiceZonesWorkflow/index.html.md)
-- [acceptInviteWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptInviteWorkflow/index.html.md)
+- [validateFulfillmentDeliverabilityStep](https://docs.medusajs.com/references/medusa-workflows/validateFulfillmentDeliverabilityStep/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)
+- [createInventoryItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInventoryItemsWorkflow/index.html.md)
+- [createInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInventoryLevelsWorkflow/index.html.md)
+- [updateInventoryItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateInventoryItemsWorkflow/index.html.md)
+- [deleteInventoryItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInventoryItemWorkflow/index.html.md)
+- [deleteInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInventoryLevelsWorkflow/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)
- [createInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInvitesWorkflow/index.html.md)
- [deleteInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInvitesWorkflow/index.html.md)
- [refreshInviteTokensWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshInviteTokensWorkflow/index.html.md)
+- [acceptInviteWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptInviteWorkflow/index.html.md)
- [deleteLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteLineItemsWorkflow/index.html.md)
-- [processPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/processPaymentWorkflow/index.html.md)
+- [createRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRefundReasonsWorkflow/index.html.md)
+- [createPaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentSessionsWorkflow/index.html.md)
+- [deletePaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePaymentSessionsWorkflow/index.html.md)
+- [deleteRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRefundReasonsWorkflow/index.html.md)
+- [updateRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRefundReasonsWorkflow/index.html.md)
+- [capturePaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/capturePaymentWorkflow/index.html.md)
- [refundPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentWorkflow/index.html.md)
+- [processPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/processPaymentWorkflow/index.html.md)
- [refundPaymentsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentsWorkflow/index.html.md)
- [validatePaymentsRefundStep](https://docs.medusajs.com/references/medusa-workflows/validatePaymentsRefundStep/index.html.md)
- [validateRefundStep](https://docs.medusajs.com/references/medusa-workflows/validateRefundStep/index.html.md)
-- [capturePaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/capturePaymentWorkflow/index.html.md)
+- [acceptOrderTransferValidationStep](https://docs.medusajs.com/references/medusa-workflows/acceptOrderTransferValidationStep/index.html.md)
+- [acceptOrderTransferWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptOrderTransferWorkflow/index.html.md)
+- [addOrderLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addOrderLineItemsWorkflow/index.html.md)
+- [archiveOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/archiveOrderWorkflow/index.html.md)
+- [beginClaimOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginClaimOrderValidationStep/index.html.md)
+- [beginClaimOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginClaimOrderWorkflow/index.html.md)
+- [beginExchangeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginExchangeOrderWorkflow/index.html.md)
+- [beginOrderEditOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditOrderWorkflow/index.html.md)
+- [beginOrderEditValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditValidationStep/index.html.md)
+- [beginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginOrderExchangeValidationStep/index.html.md)
+- [beginReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginReceiveReturnValidationStep/index.html.md)
+- [beginReceiveReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginReceiveReturnWorkflow/index.html.md)
+- [beginReturnOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginReturnOrderValidationStep/index.html.md)
+- [beginReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginReturnOrderWorkflow/index.html.md)
+- [cancelBeginOrderClaimValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderClaimValidationStep/index.html.md)
+- [cancelBeginOrderEditValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditValidationStep/index.html.md)
+- [cancelBeginOrderClaimWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderClaimWorkflow/index.html.md)
+- [cancelBeginOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditWorkflow/index.html.md)
+- [cancelBeginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeValidationStep/index.html.md)
+- [cancelClaimValidateOrderStep](https://docs.medusajs.com/references/medusa-workflows/cancelClaimValidateOrderStep/index.html.md)
+- [cancelBeginOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeWorkflow/index.html.md)
+- [cancelOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderChangeWorkflow/index.html.md)
+- [cancelOrderClaimWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderClaimWorkflow/index.html.md)
+- [cancelExchangeValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelExchangeValidateOrder/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)
+- [cancelOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderWorkflow/index.html.md)
+- [cancelOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentWorkflow/index.html.md)
+- [cancelOrderTransferRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderTransferRequestWorkflow/index.html.md)
+- [cancelReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelReceiveReturnValidationStep/index.html.md)
+- [cancelReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnReceiveWorkflow/index.html.md)
+- [cancelRequestReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelRequestReturnValidationStep/index.html.md)
+- [cancelReturnValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelReturnValidateOrder/index.html.md)
+- [cancelReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnRequestWorkflow/index.html.md)
+- [cancelReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnWorkflow/index.html.md)
+- [cancelTransferOrderRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelTransferOrderRequestValidationStep/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)
+- [cancelValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelValidateOrder/index.html.md)
+- [confirmExchangeRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestValidationStep/index.html.md)
+- [confirmClaimRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmClaimRequestValidationStep/index.html.md)
+- [confirmOrderEditRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestValidationStep/index.html.md)
+- [confirmExchangeRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestWorkflow/index.html.md)
+- [confirmReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmReceiveReturnValidationStep/index.html.md)
+- [confirmOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestWorkflow/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)
+- [confirmReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmReturnRequestValidationStep/index.html.md)
+- [createClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createClaimShippingMethodValidationStep/index.html.md)
+- [createClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createClaimShippingMethodWorkflow/index.html.md)
+- [createCompleteReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/createCompleteReturnValidationStep/index.html.md)
+- [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/index.html.md)
+- [createExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createExchangeShippingMethodWorkflow/index.html.md)
+- [createFulfillmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/createFulfillmentValidateOrder/index.html.md)
+- [createExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createExchangeShippingMethodValidationStep/index.html.md)
+- [createOrUpdateOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrUpdateOrderPaymentCollectionWorkflow/index.html.md)
+- [createOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderChangeWorkflow/index.html.md)
+- [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)
+- [createOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderEditShippingMethodWorkflow/index.html.md)
+- [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md)
+- [createOrderShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderShipmentWorkflow/index.html.md)
+- [createOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderWorkflow/index.html.md)
+- [createOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderPaymentCollectionWorkflow/index.html.md)
+- [createOrdersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrdersWorkflow/index.html.md)
+- [createReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createReturnShippingMethodValidationStep/index.html.md)
+- [declineOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/declineOrderChangeWorkflow/index.html.md)
+- [createReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnShippingMethodWorkflow/index.html.md)
+- [createShipmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/createShipmentValidateOrder/index.html.md)
+- [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)
+- [deleteOrderPaymentCollections](https://docs.medusajs.com/references/medusa-workflows/deleteOrderPaymentCollections/index.html.md)
+- [deleteOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteOrderChangeActionsWorkflow/index.html.md)
+- [dismissItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestValidationStep/index.html.md)
+- [dismissItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestWorkflow/index.html.md)
+- [exchangeAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeAddNewItemValidationStep/index.html.md)
+- [fetchShippingOptionForOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/fetchShippingOptionForOrderWorkflow/index.html.md)
+- [exchangeRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeRequestItemReturnValidationStep/index.html.md)
+- [getOrderDetailWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrderDetailWorkflow/index.html.md)
+- [getOrdersListWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrdersListWorkflow/index.html.md)
+- [markPaymentCollectionAsPaid](https://docs.medusajs.com/references/medusa-workflows/markPaymentCollectionAsPaid/index.html.md)
+- [maybeRefreshShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/maybeRefreshShippingMethodsWorkflow/index.html.md)
+- [markOrderFulfillmentAsDeliveredWorkflow](https://docs.medusajs.com/references/medusa-workflows/markOrderFulfillmentAsDeliveredWorkflow/index.html.md)
+- [orderClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimItemValidationStep/index.html.md)
+- [orderClaimAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimAddNewItemWorkflow/index.html.md)
+- [orderClaimAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimAddNewItemValidationStep/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)
+- [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)
+- [orderClaimRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimRequestItemReturnWorkflow/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)
+- [orderExchangeRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeRequestItemReturnWorkflow/index.html.md)
+- [orderFulfillmentDeliverablilityValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderFulfillmentDeliverablilityValidationStep/index.html.md)
+- [receiveAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveAndCompleteReturnOrderWorkflow/index.html.md)
+- [receiveCompleteReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/receiveCompleteReturnValidationStep/index.html.md)
+- [receiveItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestValidationStep/index.html.md)
+- [removeAddItemClaimActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeAddItemClaimActionWorkflow/index.html.md)
+- [receiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestWorkflow/index.html.md)
+- [removeClaimItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimItemActionValidationStep/index.html.md)
+- [removeClaimAddItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimAddItemActionValidationStep/index.html.md)
+- [removeClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimShippingMethodValidationStep/index.html.md)
+- [removeClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeClaimShippingMethodWorkflow/index.html.md)
+- [removeExchangeItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeItemActionValidationStep/index.html.md)
+- [removeExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeShippingMethodValidationStep/index.html.md)
+- [removeExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeExchangeShippingMethodWorkflow/index.html.md)
+- [removeItemClaimActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemClaimActionWorkflow/index.html.md)
+- [removeItemOrderEditActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemOrderEditActionWorkflow/index.html.md)
+- [removeItemExchangeActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemExchangeActionWorkflow/index.html.md)
+- [removeItemReceiveReturnActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeItemReceiveReturnActionValidationStep/index.html.md)
+- [removeItemReturnActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemReturnActionWorkflow/index.html.md)
+- [removeOrderEditItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditItemActionValidationStep/index.html.md)
+- [removeItemReceiveReturnActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemReceiveReturnActionWorkflow/index.html.md)
+- [removeReturnItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeReturnItemActionValidationStep/index.html.md)
+- [removeOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditShippingMethodValidationStep/index.html.md)
+- [removeOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditShippingMethodWorkflow/index.html.md)
+- [removeReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeReturnShippingMethodValidationStep/index.html.md)
+- [removeReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeReturnShippingMethodWorkflow/index.html.md)
+- [requestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestItemReturnWorkflow/index.html.md)
+- [requestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestItemReturnValidationStep/index.html.md)
+- [requestOrderEditRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestOrderEditRequestValidationStep/index.html.md)
+- [requestOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestOrderEditRequestWorkflow/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)
+- [requestOrderTransferValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestOrderTransferValidationStep/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)
+- [updateClaimAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimAddItemWorkflow/index.html.md)
+- [updateClaimAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimAddItemValidationStep/index.html.md)
+- [updateClaimItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemWorkflow/index.html.md)
+- [updateClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimShippingMethodWorkflow/index.html.md)
+- [updateClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimShippingMethodValidationStep/index.html.md)
+- [updateExchangeAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateExchangeAddItemValidationStep/index.html.md)
+- [updateExchangeAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateExchangeAddItemWorkflow/index.html.md)
+- [updateExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateExchangeShippingMethodWorkflow/index.html.md)
+- [updateOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderChangeActionsWorkflow/index.html.md)
+- [updateExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateExchangeShippingMethodValidationStep/index.html.md)
+- [updateOrderChangesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderChangesWorkflow/index.html.md)
+- [updateOrderEditAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditAddItemValidationStep/index.html.md)
+- [updateOrderEditAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditAddItemWorkflow/index.html.md)
+- [updateOrderEditItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityValidationStep/index.html.md)
+- [updateOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditShippingMethodWorkflow/index.html.md)
+- [updateOrderTaxLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderTaxLinesWorkflow/index.html.md)
+- [updateOrderEditItemQuantityWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityWorkflow/index.html.md)
+- [updateOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderWorkflow/index.html.md)
+- [updateOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderValidationStep/index.html.md)
+- [updateOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditShippingMethodValidationStep/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)
+- [updateRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnWorkflow/index.html.md)
+- [updateRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnValidationStep/index.html.md)
+- [updateReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReturnShippingMethodValidationStep/index.html.md)
+- [updateReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnShippingMethodWorkflow/index.html.md)
+- [updateReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReturnValidationStep/index.html.md)
+- [updateReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnWorkflow/index.html.md)
+- [validateOrderCreditLinesStep](https://docs.medusajs.com/references/medusa-workflows/validateOrderCreditLinesStep/index.html.md)
+- [createPricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPricePreferencesWorkflow/index.html.md)
+- [deletePricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePricePreferencesWorkflow/index.html.md)
+- [updatePricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePricePreferencesWorkflow/index.html.md)
- [batchPriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchPriceListPricesWorkflow/index.html.md)
- [createPriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListPricesWorkflow/index.html.md)
- [createPriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListsWorkflow/index.html.md)
- [deletePriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePriceListsWorkflow/index.html.md)
- [removePriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/removePriceListPricesWorkflow/index.html.md)
- [updatePriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePriceListPricesWorkflow/index.html.md)
-- [updatePriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePriceListsWorkflow/index.html.md)
-- [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)
-- [deleteRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRefundReasonsWorkflow/index.html.md)
-- [updateRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRefundReasonsWorkflow/index.html.md)
-- [createPricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPricePreferencesWorkflow/index.html.md)
-- [deletePricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePricePreferencesWorkflow/index.html.md)
-- [updatePricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePricePreferencesWorkflow/index.html.md)
- [batchLinkProductsToCategoryWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinkProductsToCategoryWorkflow/index.html.md)
-- [batchProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductsWorkflow/index.html.md)
+- [batchLinkProductsToCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinkProductsToCollectionWorkflow/index.html.md)
- [batchProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductVariantsWorkflow/index.html.md)
+- [updatePriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePriceListsWorkflow/index.html.md)
- [createCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCollectionsWorkflow/index.html.md)
- [createProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductOptionsWorkflow/index.html.md)
-- [batchLinkProductsToCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinkProductsToCollectionWorkflow/index.html.md)
+- [batchProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductsWorkflow/index.html.md)
- [createProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTagsWorkflow/index.html.md)
-- [createProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTypesWorkflow/index.html.md)
-- [deleteCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCollectionsWorkflow/index.html.md)
-- [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/index.html.md)
-- [deleteProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductOptionsWorkflow/index.html.md)
-- [deleteProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTagsWorkflow/index.html.md)
- [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/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)
+- [deleteCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCollectionsWorkflow/index.html.md)
+- [deleteProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductOptionsWorkflow/index.html.md)
- [deleteProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTypesWorkflow/index.html.md)
+- [deleteProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTagsWorkflow/index.html.md)
- [deleteProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductVariantsWorkflow/index.html.md)
- [importProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsWorkflow/index.html.md)
- [exportProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/exportProductsWorkflow/index.html.md)
@@ -30993,372 +31311,222 @@ Then, you pass the first sales channel ID to the `getVariantAvailability` functi
- [updateCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCollectionsWorkflow/index.html.md)
- [updateProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductOptionsWorkflow/index.html.md)
- [updateProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTypesWorkflow/index.html.md)
-- [updateProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTagsWorkflow/index.html.md)
- [updateProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductVariantsWorkflow/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)
- [upsertVariantPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/upsertVariantPricesWorkflow/index.html.md)
- [validateProductInputStep](https://docs.medusajs.com/references/medusa-workflows/validateProductInputStep/index.html.md)
-- [createProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductCategoriesWorkflow/index.html.md)
-- [updateProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductCategoriesWorkflow/index.html.md)
-- [deleteProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductCategoriesWorkflow/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)
-- [beginClaimOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginClaimOrderValidationStep/index.html.md)
-- [archiveOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/archiveOrderWorkflow/index.html.md)
-- [acceptOrderTransferWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptOrderTransferWorkflow/index.html.md)
-- [beginExchangeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginExchangeOrderWorkflow/index.html.md)
-- [beginOrderEditOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditOrderWorkflow/index.html.md)
-- [beginClaimOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginClaimOrderWorkflow/index.html.md)
-- [beginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginOrderExchangeValidationStep/index.html.md)
-- [beginReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginReceiveReturnValidationStep/index.html.md)
-- [beginReceiveReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginReceiveReturnWorkflow/index.html.md)
-- [beginReturnOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginReturnOrderValidationStep/index.html.md)
-- [beginReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginReturnOrderWorkflow/index.html.md)
-- [cancelBeginOrderClaimValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderClaimValidationStep/index.html.md)
-- [cancelBeginOrderClaimWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderClaimWorkflow/index.html.md)
-- [cancelBeginOrderEditValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditValidationStep/index.html.md)
-- [cancelBeginOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditWorkflow/index.html.md)
-- [cancelBeginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeValidationStep/index.html.md)
-- [cancelBeginOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeWorkflow/index.html.md)
-- [cancelClaimValidateOrderStep](https://docs.medusajs.com/references/medusa-workflows/cancelClaimValidateOrderStep/index.html.md)
-- [beginOrderEditValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditValidationStep/index.html.md)
-- [cancelExchangeValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelExchangeValidateOrder/index.html.md)
-- [cancelOrderClaimWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderClaimWorkflow/index.html.md)
-- [cancelOrderFulfillmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentValidateOrder/index.html.md)
-- [cancelOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentWorkflow/index.html.md)
-- [cancelOrderTransferRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderTransferRequestWorkflow/index.html.md)
-- [cancelOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderExchangeWorkflow/index.html.md)
-- [cancelOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderChangeWorkflow/index.html.md)
-- [cancelOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderWorkflow/index.html.md)
-- [cancelReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelReceiveReturnValidationStep/index.html.md)
-- [cancelReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnRequestWorkflow/index.html.md)
-- [cancelReturnValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelReturnValidateOrder/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)
-- [cancelValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelValidateOrder/index.html.md)
-- [confirmClaimRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmClaimRequestValidationStep/index.html.md)
-- [confirmClaimRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmClaimRequestWorkflow/index.html.md)
-- [cancelTransferOrderRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelTransferOrderRequestValidationStep/index.html.md)
-- [completeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeOrderWorkflow/index.html.md)
-- [confirmExchangeRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestValidationStep/index.html.md)
-- [confirmExchangeRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestWorkflow/index.html.md)
-- [confirmReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmReceiveReturnValidationStep/index.html.md)
-- [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/index.html.md)
-- [confirmOrderEditRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestValidationStep/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)
-- [confirmOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestWorkflow/index.html.md)
-- [createClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createClaimShippingMethodValidationStep/index.html.md)
-- [createAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createAndCompleteReturnOrderWorkflow/index.html.md)
-- [createClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createClaimShippingMethodWorkflow/index.html.md)
-- [createCompleteReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/createCompleteReturnValidationStep/index.html.md)
-- [createExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createExchangeShippingMethodValidationStep/index.html.md)
-- [createFulfillmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/createFulfillmentValidateOrder/index.html.md)
-- [createOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderChangeActionsWorkflow/index.html.md)
-- [createExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createExchangeShippingMethodWorkflow/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)
-- [createOrUpdateOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrUpdateOrderPaymentCollectionWorkflow/index.html.md)
-- [createOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createOrderEditShippingMethodValidationStep/index.html.md)
-- [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md)
-- [createOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderEditShippingMethodWorkflow/index.html.md)
-- [createOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderPaymentCollectionWorkflow/index.html.md)
-- [createOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderWorkflow/index.html.md)
-- [createOrdersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrdersWorkflow/index.html.md)
-- [createOrderShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderShipmentWorkflow/index.html.md)
-- [createReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createReturnShippingMethodValidationStep/index.html.md)
-- [createReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnShippingMethodWorkflow/index.html.md)
-- [createShipmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/createShipmentValidateOrder/index.html.md)
-- [declineOrderTransferRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/declineOrderTransferRequestWorkflow/index.html.md)
-- [deleteOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteOrderChangeActionsWorkflow/index.html.md)
-- [declineOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/declineOrderChangeWorkflow/index.html.md)
-- [deleteOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteOrderChangeWorkflow/index.html.md)
-- [declineTransferOrderRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/declineTransferOrderRequestValidationStep/index.html.md)
-- [deleteOrderPaymentCollections](https://docs.medusajs.com/references/medusa-workflows/deleteOrderPaymentCollections/index.html.md)
-- [dismissItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestWorkflow/index.html.md)
-- [exchangeRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeRequestItemReturnValidationStep/index.html.md)
-- [fetchShippingOptionForOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/fetchShippingOptionForOrderWorkflow/index.html.md)
-- [dismissItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestValidationStep/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)
-- [exchangeAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeAddNewItemValidationStep/index.html.md)
-- [maybeRefreshShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/maybeRefreshShippingMethodsWorkflow/index.html.md)
-- [markOrderFulfillmentAsDeliveredWorkflow](https://docs.medusajs.com/references/medusa-workflows/markOrderFulfillmentAsDeliveredWorkflow/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)
-- [orderClaimAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimAddNewItemWorkflow/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)
-- [orderClaimItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimItemWorkflow/index.html.md)
-- [orderEditAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderEditAddNewItemWorkflow/index.html.md)
-- [orderEditAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderEditAddNewItemValidationStep/index.html.md)
-- [orderEditUpdateItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderEditUpdateItemQuantityValidationStep/index.html.md)
-- [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)
-- [orderExchangeRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeRequestItemReturnWorkflow/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)
-- [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)
-- [removeClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimShippingMethodValidationStep/index.html.md)
-- [receiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestWorkflow/index.html.md)
-- [removeExchangeItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeItemActionValidationStep/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)
-- [removeClaimItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimItemActionValidationStep/index.html.md)
-- [removeExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeExchangeShippingMethodWorkflow/index.html.md)
-- [removeItemExchangeActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemExchangeActionWorkflow/index.html.md)
-- [removeItemReceiveReturnActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeItemReceiveReturnActionValidationStep/index.html.md)
-- [removeItemReceiveReturnActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemReceiveReturnActionWorkflow/index.html.md)
-- [removeItemClaimActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemClaimActionWorkflow/index.html.md)
-- [removeItemOrderEditActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemOrderEditActionWorkflow/index.html.md)
-- [removeOrderEditItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditItemActionValidationStep/index.html.md)
-- [removeItemReturnActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemReturnActionWorkflow/index.html.md)
-- [removeReturnItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeReturnItemActionValidationStep/index.html.md)
-- [removeOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditShippingMethodWorkflow/index.html.md)
-- [removeReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeReturnShippingMethodValidationStep/index.html.md)
-- [removeReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeReturnShippingMethodWorkflow/index.html.md)
-- [requestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestItemReturnValidationStep/index.html.md)
-- [requestOrderEditRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestOrderEditRequestValidationStep/index.html.md)
-- [requestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestItemReturnWorkflow/index.html.md)
-- [requestOrderTransferValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestOrderTransferValidationStep/index.html.md)
-- [removeOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditShippingMethodValidationStep/index.html.md)
-- [requestOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestOrderEditRequestWorkflow/index.html.md)
-- [throwUnlessPaymentCollectionNotPaid](https://docs.medusajs.com/references/medusa-workflows/throwUnlessPaymentCollectionNotPaid/index.html.md)
-- [updateClaimAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimAddItemWorkflow/index.html.md)
-- [requestOrderTransferWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestOrderTransferWorkflow/index.html.md)
-- [throwUnlessStatusIsNotPaid](https://docs.medusajs.com/references/medusa-workflows/throwUnlessStatusIsNotPaid/index.html.md)
-- [updateClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemValidationStep/index.html.md)
-- [updateClaimItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemWorkflow/index.html.md)
-- [updateClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimShippingMethodWorkflow/index.html.md)
-- [updateClaimAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimAddItemValidationStep/index.html.md)
-- [updateClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimShippingMethodValidationStep/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)
-- [updateExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateExchangeShippingMethodValidationStep/index.html.md)
-- [updateOrderChangesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderChangesWorkflow/index.html.md)
-- [updateExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateExchangeShippingMethodWorkflow/index.html.md)
-- [updateOrderEditAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditAddItemWorkflow/index.html.md)
-- [updateOrderEditItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityValidationStep/index.html.md)
-- [updateOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditShippingMethodValidationStep/index.html.md)
-- [updateExchangeAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateExchangeAddItemValidationStep/index.html.md)
-- [updateOrderEditItemQuantityWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityWorkflow/index.html.md)
-- [updateOrderTaxLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderTaxLinesWorkflow/index.html.md)
-- [updateOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderWorkflow/index.html.md)
-- [updateOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderValidationStep/index.html.md)
-- [updateOrderEditAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditAddItemValidationStep/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)
-- [updateOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditShippingMethodWorkflow/index.html.md)
-- [updateRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnWorkflow/index.html.md)
-- [updateRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnValidationStep/index.html.md)
-- [updateReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnWorkflow/index.html.md)
-- [updateReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReturnValidationStep/index.html.md)
-- [validateOrderCreditLinesStep](https://docs.medusajs.com/references/medusa-workflows/validateOrderCreditLinesStep/index.html.md)
-- [updateReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnShippingMethodWorkflow/index.html.md)
-- [updateReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReturnShippingMethodValidationStep/index.html.md)
-- [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)
-- [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)
-- [createReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReservationsWorkflow/index.html.md)
-- [createSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createSalesChannelsWorkflow/index.html.md)
-- [linkProductsToSalesChannelWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkProductsToSalesChannelWorkflow/index.html.md)
-- [updateSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateSalesChannelsWorkflow/index.html.md)
-- [deleteSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteSalesChannelsWorkflow/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)
-- [createReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnReasonsWorkflow/index.html.md)
-- [deleteStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteStockLocationsWorkflow/index.html.md)
-- [linkSalesChannelsToStockLocationWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkSalesChannelsToStockLocationWorkflow/index.html.md)
-- [createStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createStockLocationsWorkflow/index.html.md)
-- [createLocationFulfillmentSetWorkflow](https://docs.medusajs.com/references/medusa-workflows/createLocationFulfillmentSetWorkflow/index.html.md)
-- [updateStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateStockLocationsWorkflow/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)
-- [deleteShippingProfileWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteShippingProfileWorkflow/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)
- [addOrRemoveCampaignPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addOrRemoveCampaignPromotionsWorkflow/index.html.md)
-- [batchPromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchPromotionRulesWorkflow/index.html.md)
- [createPromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionRulesWorkflow/index.html.md)
-- [createPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionsWorkflow/index.html.md)
-- [deleteCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCampaignsWorkflow/index.html.md)
-- [deletePromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePromotionRulesWorkflow/index.html.md)
+- [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)
+- [deletePromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePromotionRulesWorkflow/index.html.md)
+- [deleteCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCampaignsWorkflow/index.html.md)
- [deletePromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePromotionsWorkflow/index.html.md)
- [updateCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCampaignsWorkflow/index.html.md)
- [updatePromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionRulesWorkflow/index.html.md)
+- [updatePromotionsStatusWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsStatusWorkflow/index.html.md)
- [updatePromotionsValidationStep](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsValidationStep/index.html.md)
- [updatePromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsWorkflow/index.html.md)
-- [updatePromotionsStatusWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsStatusWorkflow/index.html.md)
-- [createUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUserAccountWorkflow/index.html.md)
-- [createUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUsersWorkflow/index.html.md)
-- [deleteUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteUsersWorkflow/index.html.md)
-- [removeUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeUserAccountWorkflow/index.html.md)
-- [updateUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateUsersWorkflow/index.html.md)
-- [createTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRatesWorkflow/index.html.md)
+- [createProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductCategoriesWorkflow/index.html.md)
+- [deleteProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductCategoriesWorkflow/index.html.md)
+- [updateProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductCategoriesWorkflow/index.html.md)
+- [deleteReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReservationsWorkflow/index.html.md)
+- [deleteReservationsByLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReservationsByLineItemsWorkflow/index.html.md)
+- [createReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReservationsWorkflow/index.html.md)
+- [updateReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReservationsWorkflow/index.html.md)
+- [createRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRegionsWorkflow/index.html.md)
+- [deleteRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRegionsWorkflow/index.html.md)
+- [updateRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRegionsWorkflow/index.html.md)
+- [createReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnReasonsWorkflow/index.html.md)
+- [deleteReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReturnReasonsWorkflow/index.html.md)
+- [updateReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnReasonsWorkflow/index.html.md)
+- [deleteShippingProfileWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteShippingProfileWorkflow/index.html.md)
+- [validateStepShippingProfileDelete](https://docs.medusajs.com/references/medusa-workflows/validateStepShippingProfileDelete/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)
+- [createStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createStockLocationsWorkflow/index.html.md)
+- [deleteStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteStockLocationsWorkflow/index.html.md)
+- [createLocationFulfillmentSetWorkflow](https://docs.medusajs.com/references/medusa-workflows/createLocationFulfillmentSetWorkflow/index.html.md)
+- [linkSalesChannelsToStockLocationWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkSalesChannelsToStockLocationWorkflow/index.html.md)
+- [updateStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateStockLocationsWorkflow/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)
- [createTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRateRulesWorkflow/index.html.md)
- [createTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRegionsWorkflow/index.html.md)
- [deleteTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRateRulesWorkflow/index.html.md)
+- [createTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRatesWorkflow/index.html.md)
+- [deleteTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRatesWorkflow/index.html.md)
- [deleteTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRegionsWorkflow/index.html.md)
- [maybeListTaxRateRuleIdsStep](https://docs.medusajs.com/references/medusa-workflows/maybeListTaxRateRuleIdsStep/index.html.md)
+- [updateTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxRatesWorkflow/index.html.md)
- [setTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/setTaxRateRulesWorkflow/index.html.md)
- [updateTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxRegionsWorkflow/index.html.md)
-- [updateTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxRatesWorkflow/index.html.md)
-- [deleteTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRatesWorkflow/index.html.md)
+- [createUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUserAccountWorkflow/index.html.md)
+- [deleteUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteUsersWorkflow/index.html.md)
+- [createUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUsersWorkflow/index.html.md)
+- [removeUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeUserAccountWorkflow/index.html.md)
+- [updateUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateUsersWorkflow/index.html.md)
## Steps
-- [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)
+- [createApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/createApiKeysStep/index.html.md)
- [revokeApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/revokeApiKeysStep/index.html.md)
+- [linkSalesChannelsToApiKeyStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkSalesChannelsToApiKeyStep/index.html.md)
+- [updateApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateApiKeysStep/index.html.md)
- [validateSalesChannelsExistStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateSalesChannelsExistStep/index.html.md)
-- [addShippingMethodToCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/addShippingMethodToCartStep/index.html.md)
+- [setAuthAppMetadataStep](https://docs.medusajs.com/references/medusa-workflows/steps/setAuthAppMetadataStep/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)
+- [findSalesChannelStep](https://docs.medusajs.com/references/medusa-workflows/steps/findSalesChannelStep/index.html.md)
- [findOneOrAnyRegionStep](https://docs.medusajs.com/references/medusa-workflows/steps/findOneOrAnyRegionStep/index.html.md)
- [findOrCreateCustomerStep](https://docs.medusajs.com/references/medusa-workflows/steps/findOrCreateCustomerStep/index.html.md)
-- [findSalesChannelStep](https://docs.medusajs.com/references/medusa-workflows/steps/findSalesChannelStep/index.html.md)
- [getActionsToComputeFromPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getActionsToComputeFromPromotionsStep/index.html.md)
- [getLineItemActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getLineItemActionsStep/index.html.md)
-- [getPromotionCodesToApply](https://docs.medusajs.com/references/medusa-workflows/steps/getPromotionCodesToApply/index.html.md)
- [getVariantPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantPriceSetsStep/index.html.md)
+- [getPromotionCodesToApply](https://docs.medusajs.com/references/medusa-workflows/steps/getPromotionCodesToApply/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)
+- [prepareAdjustmentsFromPromotionActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/prepareAdjustmentsFromPromotionActionsStep/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)
+- [reserveInventoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/reserveInventoryStep/index.html.md)
- [retrieveCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/retrieveCartStep/index.html.md)
-- [updateCartPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartPromotionsStep/index.html.md)
- [setTaxLinesForItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/setTaxLinesForItemsStep/index.html.md)
-- [updateCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartsStep/index.html.md)
+- [updateCartPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartPromotionsStep/index.html.md)
- [updateShippingMethodsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingMethodsStep/index.html.md)
+- [updateLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateLineItemsStep/index.html.md)
+- [updateCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartsStep/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)
-- [updateLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateLineItemsStep/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)
+- [validateVariantPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateVariantPricesStep/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)
-- [validateCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartStep/index.html.md)
-- [updateApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateApiKeysStep/index.html.md)
-- [setAuthAppMetadataStep](https://docs.medusajs.com/references/medusa-workflows/steps/setAuthAppMetadataStep/index.html.md)
-- [createCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomerAddressesStep/index.html.md)
-- [deleteCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomerAddressesStep/index.html.md)
-- [deleteCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomersStep/index.html.md)
-- [maybeUnsetDefaultBillingAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/maybeUnsetDefaultBillingAddressesStep/index.html.md)
-- [maybeUnsetDefaultShippingAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/maybeUnsetDefaultShippingAddressesStep/index.html.md)
-- [updateCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomerAddressesStep/index.html.md)
-- [createCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomersStep/index.html.md)
-- [updateCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomersStep/index.html.md)
-- [validateCustomerAccountCreation](https://docs.medusajs.com/references/medusa-workflows/steps/validateCustomerAccountCreation/index.html.md)
+- [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)
- [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)
-- [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)
- [useQueryGraphStep](https://docs.medusajs.com/references/medusa-workflows/steps/useQueryGraphStep/index.html.md)
-- [createRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRemoteLinkStep/index.html.md)
+- [validatePresenceOfStep](https://docs.medusajs.com/references/medusa-workflows/steps/validatePresenceOfStep/index.html.md)
+- [useRemoteQueryStep](https://docs.medusajs.com/references/medusa-workflows/steps/useRemoteQueryStep/index.html.md)
+- [deleteCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomerAddressesStep/index.html.md)
+- [createCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomersStep/index.html.md)
+- [createCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomerAddressesStep/index.html.md)
+- [deleteCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomersStep/index.html.md)
+- [maybeUnsetDefaultBillingAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/maybeUnsetDefaultBillingAddressesStep/index.html.md)
+- [updateCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomerAddressesStep/index.html.md)
+- [maybeUnsetDefaultShippingAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/maybeUnsetDefaultShippingAddressesStep/index.html.md)
+- [updateCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomersStep/index.html.md)
+- [validateCustomerAccountCreation](https://docs.medusajs.com/references/medusa-workflows/steps/validateCustomerAccountCreation/index.html.md)
+- [createDefaultStoreStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultStoreStep/index.html.md)
+- [validateDraftOrderStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateDraftOrderStep/index.html.md)
- [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)
- [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)
-- [createDefaultStoreStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultStoreStep/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)
-- [adjustInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/adjustInventoryLevelsStep/index.html.md)
-- [deleteInventoryItemStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInventoryItemStep/index.html.md)
-- [createInventoryItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInventoryItemsStep/index.html.md)
-- [deleteInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInventoryLevelsStep/index.html.md)
-- [updateInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryLevelsStep/index.html.md)
-- [updateInventoryItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryItemsStep/index.html.md)
-- [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)
-- [validateInventoryDeleteStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryDeleteStep/index.html.md)
-- [validateDraftOrderStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateDraftOrderStep/index.html.md)
+- [calculateShippingOptionsPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/calculateShippingOptionsPricesStep/index.html.md)
+- [buildPriceSet](https://docs.medusajs.com/references/medusa-workflows/steps/buildPriceSet/index.html.md)
+- [cancelFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelFulfillmentStep/index.html.md)
+- [createFulfillmentSets](https://docs.medusajs.com/references/medusa-workflows/steps/createFulfillmentSets/index.html.md)
+- [createFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/createFulfillmentStep/index.html.md)
+- [createReturnFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnFulfillmentStep/index.html.md)
+- [createServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createServiceZonesStep/index.html.md)
+- [createShippingOptionsPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingOptionsPriceSetsStep/index.html.md)
+- [createShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingProfilesStep/index.html.md)
+- [createShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingOptionRulesStep/index.html.md)
+- [deleteFulfillmentSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteFulfillmentSetsStep/index.html.md)
+- [deleteShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingOptionsStep/index.html.md)
+- [deleteServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteServiceZonesStep/index.html.md)
+- [deleteShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingOptionRulesStep/index.html.md)
+- [setShippingOptionsPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/setShippingOptionsPricesStep/index.html.md)
+- [updateServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateServiceZonesStep/index.html.md)
+- [updateFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateFulfillmentStep/index.html.md)
+- [updateShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingOptionRulesStep/index.html.md)
+- [updateShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingProfilesStep/index.html.md)
+- [validateShipmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShipmentStep/index.html.md)
+- [upsertShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/upsertShippingOptionsStep/index.html.md)
+- [validateShippingOptionPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShippingOptionPricesStep/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)
- [createInviteStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInviteStep/index.html.md)
- [deleteInvitesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInvitesStep/index.html.md)
- [refreshInviteTokensStep](https://docs.medusajs.com/references/medusa-workflows/steps/refreshInviteTokensStep/index.html.md)
- [validateTokenStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateTokenStep/index.html.md)
-- [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)
+- [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)
+- [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)
+- [deleteInventoryItemStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInventoryItemStep/index.html.md)
+- [validateInventoryItemsForCreate](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryItemsForCreate/index.html.md)
+- [updateInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryLevelsStep/index.html.md)
+- [validateInventoryLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryLocationsStep/index.html.md)
+- [validateInventoryDeleteStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryDeleteStep/index.html.md)
- [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)
-- [uploadFilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/uploadFilesStep/index.html.md)
+- [notifyOnFailureStep](https://docs.medusajs.com/references/medusa-workflows/steps/notifyOnFailureStep/index.html.md)
+- [sendNotificationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/sendNotificationsStep/index.html.md)
+- [authorizePaymentSessionStep](https://docs.medusajs.com/references/medusa-workflows/steps/authorizePaymentSessionStep/index.html.md)
+- [capturePaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/capturePaymentStep/index.html.md)
+- [cancelPaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelPaymentStep/index.html.md)
+- [refundPaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/refundPaymentStep/index.html.md)
+- [refundPaymentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/refundPaymentsStep/index.html.md)
- [addOrderTransactionStep](https://docs.medusajs.com/references/medusa-workflows/steps/addOrderTransactionStep/index.html.md)
- [archiveOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/archiveOrdersStep/index.html.md)
- [cancelOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderChangeStep/index.html.md)
- [cancelOrderClaimStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderClaimStep/index.html.md)
- [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)
+- [completeOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/completeOrdersStep/index.html.md)
+- [cancelOrderReturnStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderReturnStep/index.html.md)
- [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)
- [createOrderExchangeItemsFromActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderExchangeItemsFromActionsStep/index.html.md)
-- [createOrderLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderLineItemsStep/index.html.md)
- [createOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrdersStep/index.html.md)
-- [createReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnsStep/index.html.md)
+- [createOrderLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderLineItemsStep/index.html.md)
- [createOrderExchangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderExchangesStep/index.html.md)
- [declineOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/declineOrderChangeStep/index.html.md)
-- [deleteOrderChangeActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderChangeActionsStep/index.html.md)
- [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)
+- [createReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnsStep/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)
- [deleteOrderLineItems](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderLineItems/index.html.md)
+- [deleteOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderChangesStep/index.html.md)
- [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)
-- [deleteExchangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteExchangesStep/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)
- [registerOrderShipmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderShipmentStep/index.html.md)
-- [registerOrderDeliveryStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderDeliveryStep/index.html.md)
- [setOrderTaxLinesForItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/setOrderTaxLinesForItemsStep/index.html.md)
-- [registerOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderChangesStep/index.html.md)
-- [updateOrderShippingMethodsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderShippingMethodsStep/index.html.md)
- [updateOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderChangesStep/index.html.md)
-- [updateReturnItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnItemsStep/index.html.md)
- [updateOrderChangeActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderChangeActionsStep/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)
-- [createPaymentAccountHolderStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentAccountHolderStep/index.html.md)
-- [createRefundReasonStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRefundReasonStep/index.html.md)
- [updateReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnsStep/index.html.md)
-- [createPaymentSessionStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentSessionStep/index.html.md)
-- [deletePaymentSessionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePaymentSessionsStep/index.html.md)
-- [validateDeletedPaymentSessionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateDeletedPaymentSessionsStep/index.html.md)
-- [updatePaymentCollectionStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePaymentCollectionStep/index.html.md)
-- [updateRefundReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRefundReasonsStep/index.html.md)
-- [deleteRefundReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteRefundReasonsStep/index.html.md)
-- [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)
-- [removePriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/removePriceListPricesStep/index.html.md)
-- [updatePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceListsStep/index.html.md)
-- [updatePriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceListPricesStep/index.html.md)
-- [getExistingPriceListsPriceIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getExistingPriceListsPriceIdsStep/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)
+- [updateReturnItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnItemsStep/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)
@@ -31366,116 +31534,106 @@ Then, you pass the first sales channel ID to the `getVariantAvailability` functi
- [updatePricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePricePreferencesStep/index.html.md)
- [updatePriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceSetsStep/index.html.md)
- [batchLinkProductsToCategoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/batchLinkProductsToCategoryStep/index.html.md)
-- [batchLinkProductsToCollectionStep](https://docs.medusajs.com/references/medusa-workflows/steps/batchLinkProductsToCollectionStep/index.html.md)
- [createCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCollectionsStep/index.html.md)
+- [batchLinkProductsToCollectionStep](https://docs.medusajs.com/references/medusa-workflows/steps/batchLinkProductsToCollectionStep/index.html.md)
- [createProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductOptionsStep/index.html.md)
- [createProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductTagsStep/index.html.md)
- [createProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductTypesStep/index.html.md)
-- [createProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductVariantsStep/index.html.md)
- [createProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductsStep/index.html.md)
+- [createProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductVariantsStep/index.html.md)
- [createVariantPricingLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/createVariantPricingLinkStep/index.html.md)
- [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)
- [deleteProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductVariantsStep/index.html.md)
+- [deleteProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductTypesStep/index.html.md)
+- [deleteProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductTagsStep/index.html.md)
- [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)
+- [updateCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCollectionsStep/index.html.md)
- [normalizeCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/normalizeCsvStep/index.html.md)
- [parseProductCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/parseProductCsvStep/index.html.md)
-- [updateCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCollectionsStep/index.html.md)
-- [updateProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductOptionsStep/index.html.md)
-- [updateProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductTagsStep/index.html.md)
- [updateProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductTypesStep/index.html.md)
+- [updateProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductOptionsStep/index.html.md)
- [updateProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductVariantsStep/index.html.md)
+- [updateProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductTagsStep/index.html.md)
- [updateProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductsStep/index.html.md)
- [waitConfirmationProductImportStep](https://docs.medusajs.com/references/medusa-workflows/steps/waitConfirmationProductImportStep/index.html.md)
-- [addCampaignPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/addCampaignPromotionsStep/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)
+- [updatePriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceListPricesStep/index.html.md)
+- [removePriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/removePriceListPricesStep/index.html.md)
+- [validatePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validatePriceListsStep/index.html.md)
+- [validateVariantPriceLinksStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateVariantPriceLinksStep/index.html.md)
+- [createPaymentSessionStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentSessionStep/index.html.md)
+- [createRefundReasonStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRefundReasonStep/index.html.md)
+- [deletePaymentSessionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePaymentSessionsStep/index.html.md)
+- [createPaymentAccountHolderStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentAccountHolderStep/index.html.md)
+- [updateRefundReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRefundReasonsStep/index.html.md)
+- [updatePaymentCollectionStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePaymentCollectionStep/index.html.md)
+- [deleteRefundReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteRefundReasonsStep/index.html.md)
+- [validateDeletedPaymentSessionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateDeletedPaymentSessionsStep/index.html.md)
+- [deleteProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductCategoriesStep/index.html.md)
+- [createProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductCategoriesStep/index.html.md)
+- [updateProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductCategoriesStep/index.html.md)
- [addRulesToPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/addRulesToPromotionsStep/index.html.md)
+- [addCampaignPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/addCampaignPromotionsStep/index.html.md)
- [createCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCampaignsStep/index.html.md)
- [createPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPromotionsStep/index.html.md)
- [deleteCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCampaignsStep/index.html.md)
-- [deletePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePromotionsStep/index.html.md)
- [removeCampaignPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeCampaignPromotionsStep/index.html.md)
-- [removeRulesFromPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeRulesFromPromotionsStep/index.html.md)
+- [deletePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePromotionsStep/index.html.md)
- [updateCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCampaignsStep/index.html.md)
+- [removeRulesFromPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeRulesFromPromotionsStep/index.html.md)
- [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)
-- [buildPriceSet](https://docs.medusajs.com/references/medusa-workflows/steps/buildPriceSet/index.html.md)
-- [calculateShippingOptionsPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/calculateShippingOptionsPricesStep/index.html.md)
-- [cancelFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelFulfillmentStep/index.html.md)
-- [createFulfillmentSets](https://docs.medusajs.com/references/medusa-workflows/steps/createFulfillmentSets/index.html.md)
-- [createFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/createFulfillmentStep/index.html.md)
-- [createReturnFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnFulfillmentStep/index.html.md)
-- [createServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createServiceZonesStep/index.html.md)
-- [createShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingOptionRulesStep/index.html.md)
-- [createShippingOptionsPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingOptionsPriceSetsStep/index.html.md)
-- [createShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingProfilesStep/index.html.md)
-- [deleteFulfillmentSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteFulfillmentSetsStep/index.html.md)
-- [deleteServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteServiceZonesStep/index.html.md)
-- [deleteShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingOptionRulesStep/index.html.md)
-- [deleteShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingOptionsStep/index.html.md)
-- [setShippingOptionsPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/setShippingOptionsPricesStep/index.html.md)
-- [updateFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateFulfillmentStep/index.html.md)
-- [updateServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateServiceZonesStep/index.html.md)
-- [updateShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingOptionRulesStep/index.html.md)
-- [updateShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingProfilesStep/index.html.md)
-- [upsertShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/upsertShippingOptionsStep/index.html.md)
-- [validateShipmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShipmentStep/index.html.md)
-- [validateShippingOptionPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShippingOptionPricesStep/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)
+- [updateRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRegionsStep/index.html.md)
+- [deleteRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteRegionsStep/index.html.md)
+- [deleteReservationsByLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReservationsByLineItemsStep/index.html.md)
+- [createReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReservationsStep/index.html.md)
+- [deleteReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReservationsStep/index.html.md)
+- [updateReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReservationsStep/index.html.md)
+- [associateProductsWithSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/associateProductsWithSalesChannelsStep/index.html.md)
+- [canDeleteSalesChannelsOrThrowStep](https://docs.medusajs.com/references/medusa-workflows/steps/canDeleteSalesChannelsOrThrowStep/index.html.md)
+- [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)
+- [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)
+- [createSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createSalesChannelsStep/index.html.md)
+- [detachProductsFromSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/detachProductsFromSalesChannelsStep/index.html.md)
+- [updateSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateSalesChannelsStep/index.html.md)
+- [listShippingOptionsForContextStep](https://docs.medusajs.com/references/medusa-workflows/steps/listShippingOptionsForContextStep/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)
-- [createReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReservationsStep/index.html.md)
-- [deleteReservationsByLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReservationsByLineItemsStep/index.html.md)
-- [deleteReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReservationsStep/index.html.md)
-- [updateReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReservationsStep/index.html.md)
-- [associateLocationsWithSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/associateLocationsWithSalesChannelsStep/index.html.md)
-- [associateProductsWithSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/associateProductsWithSalesChannelsStep/index.html.md)
-- [canDeleteSalesChannelsOrThrowStep](https://docs.medusajs.com/references/medusa-workflows/steps/canDeleteSalesChannelsOrThrowStep/index.html.md)
-- [createDefaultSalesChannelStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultSalesChannelStep/index.html.md)
-- [createSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createSalesChannelsStep/index.html.md)
-- [deleteSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteSalesChannelsStep/index.html.md)
-- [detachProductsFromSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/detachProductsFromSalesChannelsStep/index.html.md)
-- [updateSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateSalesChannelsStep/index.html.md)
- [deleteShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingProfilesStep/index.html.md)
-- [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)
- [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)
-- [deleteRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteRegionsStep/index.html.md)
-- [createRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRegionsStep/index.html.md)
-- [updateRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRegionsStep/index.html.md)
-- [setRegionsPaymentProvidersStep](https://docs.medusajs.com/references/medusa-workflows/steps/setRegionsPaymentProvidersStep/index.html.md)
+- [deleteStockLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteStockLocationsStep/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)
-- [authorizePaymentSessionStep](https://docs.medusajs.com/references/medusa-workflows/steps/authorizePaymentSessionStep/index.html.md)
-- [cancelPaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelPaymentStep/index.html.md)
-- [capturePaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/capturePaymentStep/index.html.md)
-- [refundPaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/refundPaymentStep/index.html.md)
-- [refundPaymentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/refundPaymentsStep/index.html.md)
-- [listShippingOptionsForContextStep](https://docs.medusajs.com/references/medusa-workflows/steps/listShippingOptionsForContextStep/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)
+- [deleteTaxRateRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRateRulesStep/index.html.md)
- [createTaxRateRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRateRulesStep/index.html.md)
- [createTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRatesStep/index.html.md)
- [createTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRegionsStep/index.html.md)
-- [deleteTaxRateRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRateRulesStep/index.html.md)
- [deleteTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRatesStep/index.html.md)
- [deleteTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRegionsStep/index.html.md)
-- [getItemTaxLinesStep](https://docs.medusajs.com/references/medusa-workflows/steps/getItemTaxLinesStep/index.html.md)
-- [listTaxRateIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listTaxRateIdsStep/index.html.md)
- [listTaxRateRuleIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listTaxRateRuleIdsStep/index.html.md)
+- [listTaxRateIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listTaxRateIdsStep/index.html.md)
+- [getItemTaxLinesStep](https://docs.medusajs.com/references/medusa-workflows/steps/getItemTaxLinesStep/index.html.md)
- [updateTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateTaxRatesStep/index.html.md)
- [updateTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateTaxRegionsStep/index.html.md)
-- [detachLocationsFromSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/detachLocationsFromSalesChannelsStep/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)
# Events Reference
@@ -32919,180 +33077,12 @@ npx medusa --help
***
-# build Command - Medusa CLI Reference
+# develop 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.
+Start Medusa application in development. This command watches files for any changes, then rebuilds the files and restarts the Medusa application.
```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.
-
-
-# exec Command - Medusa CLI Reference
-
-Run a custom CLI script. Learn more about it in [this guide](https://docs.medusajs.com/docs/learn/fundamentals/custom-cli-scripts/index.html.md).
-
-```bash
-npx medusa exec [file] [args...]
-```
-
-## Arguments
-
-|Argument|Description|Required|
-|---|---|---|---|---|
-|\`file\`|The path to the TypeScript or JavaScript file holding the function to execute.|Yes|
-|\`args\`|A list of arguments to pass to the function. These arguments are passed in the |No|
-
-
-# new Command - Medusa CLI Reference
-
-Create a new Medusa application. Unlike the `create-medusa-app` CLI tool, this command provides more flexibility for experienced Medusa developers in creating and configuring their project.
-
-```bash
-medusa new [ []]
-```
-
-## Arguments
-
-|Argument|Description|Required|Default|
-|---|---|---|---|---|---|---|
-|\`dir\_name\`|The name of the directory to create the Medusa application in.|Yes|-|
-|\`starter\_url\`|The URL of the starter repository to create the project from.|No|\`https://github.com/medusajs/medusa-starter-default\`|
-
-## Options
-
-|Option|Description|
-|---|---|---|
-|\`-y\`|Skip all prompts, such as databaes prompts. A database might not be created if default PostgreSQL credentials don't work.|
-|\`--skip-db\`|Skip database creation.|
-|\`--skip-env\`|Skip populating |
-|\`--db-user \\`|The database user to use for database setup.|
-|\`--db-database \\`|The name of the database used for database setup.|
-|\`--db-pass \\`|The database password to use for database setup.|
-|\`--db-port \\`|The database port to use for database setup.|
-|\`--db-host \\`|The database host to use for database setup.|
-
-
-# plugin Commands - Medusa CLI Reference
-
-Commands starting with `plugin:` perform actions related to [plugin](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md) development.
-
-These commands are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0).
-
-## plugin:publish
-
-Publish a plugin into the local packages registry. The command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. You can then install the plugin in a local Medusa project using the [plugin:add](#pluginadd) command.
-
-```bash
-npx medusa plugin:publish
-```
-
-***
-
-## plugin:add
-
-Install the specified plugins from the local package registry into a local Medusa application. Plugins can be added to the local package registry using the [plugin:publish](#pluginpublish) command.
-
-```bash
-npx medusa plugin:add [names...]
-```
-
-### Arguments
-
-|Argument|Description|Required|
-|---|---|---|---|---|
-|\`names\`|The names of one or more plugins to install from the local package registry. A plugin's name is as specified in its |Yes|
-
-***
-
-## plugin:develop
-
-Start a development server for a plugin. The command will watch for changes in the plugin's source code and automatically re-publish the changes into the local package registry.
-
-```bash
-npx medusa plugin:develop
-```
-
-***
-
-## plugin:db:generate
-
-Generate migrations for all modules in a plugin.
-
-```bash
-npx medusa plugin:db:generate
-```
-
-***
-
-## plugin:build
-
-Build a plugin before publishing it to NPM. The command will compile an output in the `.medusa/server` directory.
-
-```bash
-npx medusa plugin:build
-```
-
-
-# start Command - Medusa CLI Reference
-
-Start the Medusa application in production.
-
-```bash
-npx medusa start
+npx medusa develop
```
## Options
@@ -33101,7 +33091,6 @@ npx medusa start
|---|---|---|---|---|
|\`-H \\`|Set host of the Medusa server.|\`localhost\`|
|\`-p \\`|Set port of the Medusa server.|\`9000\`|
-|\`--cluster \\`|Start Medusa's Node.js server in |Cluster mode is disabled by default. If the option is passed but no number is passed, Medusa will try to consume all available CPU cores.|
# db Commands - Medusa CLI Reference
@@ -33224,12 +33213,180 @@ npx medusa db:sync-links
|\`--execute-all\`|Skip prompts when syncing links and execute all (including unsafe) actions.|No|Prompts are shown for unsafe actions, by default.|
-# develop Command - Medusa CLI Reference
+# build 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.
+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 develop
+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.
+
+
+# new Command - Medusa CLI Reference
+
+Create a new Medusa application. Unlike the `create-medusa-app` CLI tool, this command provides more flexibility for experienced Medusa developers in creating and configuring their project.
+
+```bash
+medusa new [ []]
+```
+
+## Arguments
+
+|Argument|Description|Required|Default|
+|---|---|---|---|---|---|---|
+|\`dir\_name\`|The name of the directory to create the Medusa application in.|Yes|-|
+|\`starter\_url\`|The URL of the starter repository to create the project from.|No|\`https://github.com/medusajs/medusa-starter-default\`|
+
+## Options
+
+|Option|Description|
+|---|---|---|
+|\`-y\`|Skip all prompts, such as databaes prompts. A database might not be created if default PostgreSQL credentials don't work.|
+|\`--skip-db\`|Skip database creation.|
+|\`--skip-env\`|Skip populating |
+|\`--db-user \\`|The database user to use for database setup.|
+|\`--db-database \\`|The name of the database used for database setup.|
+|\`--db-pass \\`|The database password to use for database setup.|
+|\`--db-port \\`|The database port to use for database setup.|
+|\`--db-host \\`|The database host to use for database setup.|
+
+
+# plugin Commands - Medusa CLI Reference
+
+Commands starting with `plugin:` perform actions related to [plugin](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md) development.
+
+These commands are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0).
+
+## plugin:publish
+
+Publish a plugin into the local packages registry. The command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. You can then install the plugin in a local Medusa project using the [plugin:add](#pluginadd) command.
+
+```bash
+npx medusa plugin:publish
+```
+
+***
+
+## plugin:add
+
+Install the specified plugins from the local package registry into a local Medusa application. Plugins can be added to the local package registry using the [plugin:publish](#pluginpublish) command.
+
+```bash
+npx medusa plugin:add [names...]
+```
+
+### Arguments
+
+|Argument|Description|Required|
+|---|---|---|---|---|
+|\`names\`|The names of one or more plugins to install from the local package registry. A plugin's name is as specified in its |Yes|
+
+***
+
+## plugin:develop
+
+Start a development server for a plugin. The command will watch for changes in the plugin's source code and automatically re-publish the changes into the local package registry.
+
+```bash
+npx medusa plugin:develop
+```
+
+***
+
+## plugin:db:generate
+
+Generate migrations for all modules in a plugin.
+
+```bash
+npx medusa plugin:db:generate
+```
+
+***
+
+## plugin:build
+
+Build a plugin before publishing it to NPM. The command will compile an output in the `.medusa/server` directory.
+
+```bash
+npx medusa plugin:build
+```
+
+
+# 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|
+
+
+# start Command - Medusa CLI Reference
+
+Start the Medusa application in production.
+
+```bash
+npx medusa start
```
## Options
@@ -33238,6 +33395,7 @@ npx medusa develop
|---|---|---|---|---|
|\`-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
@@ -33480,33 +33638,20 @@ 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
+# develop 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.
+Start Medusa application in development. This command watches files for any changes, then rebuilds the files and restarts the Medusa application.
```bash
-medusa new [ []]
+npx medusa develop
```
-## 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.|
+|Option|Description|Default|
+|---|---|---|---|---|
+|\`-H \\`|Set host of the Medusa server.|\`localhost\`|
+|\`-p \\`|Set port of the Medusa server.|\`9000\`|
# plugin Commands - Medusa CLI Reference
@@ -33570,20 +33715,33 @@ npx medusa plugin:build
```
-# develop Command - Medusa CLI Reference
+# new 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.
+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
-npx medusa develop
+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|Default|
-|---|---|---|---|---|
-|\`-H \\`|Set host of the Medusa server.|\`localhost\`|
-|\`-p \\`|Set port of the Medusa server.|\`9000\`|
+|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
@@ -42793,32 +42951,659 @@ 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 Loyalty Points System in Medusa
+# Send Abandoned Cart Notifications in Medusa
-In this tutorial, you'll learn how to implement a loyalty points system in Medusa.
+In this tutorial, you will learn how to send notifications to customers who have abandoned their carts.
-Medusa Cloud provides a beta Store Credits feature that facilitates building a loyalty point system. [Get in touch](https://medusajs.com/contact) for early access.
+When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md), which are available out-of-the-box. These features include cart-management capabilities.
-When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md), which are available out-of-the-box. These features include management capabilities related to carts, orders, promotions, and more.
+Medusa's [Notification Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/index.html.md) allows you to send notifications to users or customers, such as password reset emails, order confirmation SMS, or other types of notifications.
-A loyalty point system allows customers to earn points for purchases, which can be redeemed for discounts or rewards. In this tutorial, you'll learn how to customize the Medusa application to implement a loyalty points system.
-
-You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer.
+In this tutorial, you will use the Notification Module to send an email to customers who have abandoned their carts. The email will contain a link to recover the customer's cart, encouraging them to complete their purchase. You will use SendGrid to send the emails, but you can also use other email providers.
## Summary
-By following this tutorial, you will learn how to:
+By following this tutorial, you will:
- Install and set up Medusa.
-- Define models to store loyalty points and the logic to manage them.
-- Build flows that allow customers to earn and redeem points during checkout.
- - Points are redeemed through dynamic promotions specific to the customer.
-- Customize the cart completion flow to validate applied loyalty points.
+- Create the logic to send an email to customers who have abandoned their carts.
+- Run the above logic once a day.
+- Add a route to the storefront to recover the cart.
-
+
-- [Loyalty Points Repository](https://github.com/medusajs/examples/tree/main/loyalty-points): Find the full code for this guide in this repository.
-- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1744212595/OpenApi/Loyalty-Points_jwi5e9.yaml): Import this OpenApi Specs file into tools like Postman.
+[View on Github](https://github.com/medusajs/examples/tree/main/abandoned-cart): Find the full code for this tutorial.
+
+***
+
+## Step 1: Install a Medusa Application
+
+### Prerequisites
+
+- [Node.js v20+](https://nodejs.org/en/download)
+- [Git CLI tool](https://git-scm.com/downloads)
+- [PostgreSQL](https://www.postgresql.org/download/)
+
+Start by installing the Medusa application on your machine with the following command:
+
+```bash
+npx create-medusa-app@latest
+```
+
+You will first be asked for the project's name. Then, when asked whether you want to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose "Yes."
+
+Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name and the Next.js Starter Storefront in a separate directory with the `{project-name}-storefront` name.
+
+The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).
+
+Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterwards, you can log in with the new user and explore the dashboard.
+
+Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.
+
+***
+
+## Step 2: Set up SendGrid
+
+### Prerequisites
+
+- [SendGrid account](https://sendgrid.com)
+- [Verified Sender Identity](https://mc.sendgrid.com/senders)
+- [SendGrid API Key](https://app.sendgrid.com/settings/api_keys)
+
+Medusa's Notification Module provides the general functionality to send notifications, but the sending logic is implemented in a module provider. This allows you to integrate the email provider of your choice.
+
+To send the cart-abandonment emails, you will use SendGrid. Medusa provides a [SendGrid Notification Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/sendgrid/index.html.md) that you can use to send emails.
+
+Alternatively, you can use [other Notification Module Providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification#what-is-a-notification-module-provider/index.html.md) or [create a custom provider](https://docs.medusajs.com/references/notification-provider-module/index.html.md).
+
+To set up SendGrid, add the SendGrid Notification Module Provider to `medusa-config.ts`:
+
+```ts title="medusa-config.ts"
+module.exports = defineConfig({
+ // ...
+ modules: [
+ {
+ resolve: "@medusajs/medusa/notification",
+ options: {
+ providers: [
+ {
+ resolve: "@medusajs/medusa/notification-sendgrid",
+ id: "sendgrid",
+ options: {
+ channels: ["email"],
+ api_key: process.env.SENDGRID_API_KEY,
+ from: process.env.SENDGRID_FROM,
+ },
+ },
+ ],
+ },
+ },
+ ],
+})
+```
+
+In the `modules` configuration, you pass the Notification Provider and add SendGrid as a provider. You also pass to the SendGrid Module Provider the following options:
+
+- `channels`: The channels that the provider supports. In this case, it is only email.
+- `api_key`: Your SendGrid API key.
+- `from`: The email address that the emails will be sent from.
+
+Then, set the SendGrid API key and "from" email as environment variables, such as in the `.env` file at the root of your project:
+
+```plain
+SENDGRID_API_KEY=your-sendgrid-api-key
+SENDGRID_FROM=test@gmail.com
+```
+
+You can now use SendGrid to send emails in Medusa.
+
+***
+
+## Step 3: Send Abandoned Cart Notification Flow
+
+You will now implement the sending logic for the abandoned cart notifications.
+
+To build custom commerce features in Medusa, you create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow like you construct a function, but it is a special function that allows you to track its executions' progress, define roll-back logic, and configure other advanced features. Then, you execute the workflow from other customizations, such as in a scheduled job.
+
+In this step, you will create the workflow that sends the abandoned cart notifications. Later, you will learn how to execute it once a day.
+
+The workflow will receive the list of abandoned carts as an input. The workflow has the following steps:
+
+- [sendAbandonedNotificationsStep](#sendAbandonedNotificationsStep): Send the abandoned cart notifications.
+- [updateCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartsStep/index.html.md): Update the cart to store the last notification date.
+
+Medusa provides the second step in its `@medusajs/medusa/core-flows` package. So, you only need to implement the first one.
+
+### sendAbandonedNotificationsStep
+
+The first step of the workflow sends a notification to the owners of the abandoned carts that are passed as an input.
+
+To implement the step, create the file `src/workflows/steps/send-abandoned-notifications.ts` with the following content:
+
+```ts title="src/workflows/steps/send-abandoned-notifications.ts"
+import {
+ createStep,
+ StepResponse,
+} from "@medusajs/framework/workflows-sdk"
+import { Modules } from "@medusajs/framework/utils"
+import { CartDTO, CustomerDTO } from "@medusajs/framework/types"
+
+type SendAbandonedNotificationsStepInput = {
+ carts: (CartDTO & {
+ customer: CustomerDTO
+ })[]
+}
+
+export const sendAbandonedNotificationsStep = createStep(
+ "send-abandoned-notifications",
+ async (input: SendAbandonedNotificationsStepInput, { container }) => {
+ const notificationModuleService = container.resolve(
+ Modules.NOTIFICATION
+ )
+
+ const notificationData = input.carts.map((cart) => ({
+ to: cart.email!,
+ channel: "email",
+ template: process.env.ABANDONED_CART_TEMPLATE_ID || "",
+ data: {
+ customer: {
+ first_name: cart.customer?.first_name || cart.shipping_address?.first_name,
+ last_name: cart.customer?.last_name || cart.shipping_address?.last_name,
+ },
+ cart_id: cart.id,
+ items: cart.items?.map((item) => ({
+ product_title: item.title,
+ quantity: item.quantity,
+ unit_price: item.unit_price,
+ thumbnail: item.thumbnail,
+ })),
+ },
+ }))
+
+ const notifications = await notificationModuleService.createNotifications(
+ notificationData
+ )
+
+ return new StepResponse({
+ notifications,
+ })
+ }
+)
+```
+
+You create a step with `createStep` from the Workflows SDK. It accepts two parameters:
+
+1. The step's unique name, which is `create-review`.
+2. An async function that receives two parameters:
+ - The step's input, which is in this case an object with the review's properties.
+ - An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step.
+
+In the step function, you first resolve the Notification Module's service, which has methods to manage notifications. Then, you prepare the data of each notification, and create the notifications with the `createNotifications` method.
+
+Notice that each notification is an object with the following properties:
+
+- `to`: The email address of the customer.
+- `channel`: The channel that the notification will be sent through. The Notification Module uses the provider registered for the channel.
+- `template`: The ID or name of the email template in the third-party provider. Make sure to set it as an environment variable once you have it.
+- `data`: The data to pass to the template to render the email's dynamic content.
+
+Based on the dynamic template you create in SendGrid or another provider, you can pass different data in the `data` object.
+
+A step function must return a `StepResponse` instance. The `StepResponse` constructor accepts the step's output as a parameter, which is the created notifications.
+
+### Create Workflow
+
+You can now create the workflow that uses the step you just created to send the abandoned cart notifications.
+
+Create the file `src/workflows/send-abandoned-carts.ts` with the following content:
+
+```ts title="src/workflows/send-abandoned-carts.ts"
+import {
+ createWorkflow,
+ WorkflowResponse,
+ transform,
+} from "@medusajs/framework/workflows-sdk"
+import {
+ sendAbandonedNotificationsStep,
+} from "./steps/send-abandoned-notifications"
+import { updateCartsStep } from "@medusajs/medusa/core-flows"
+import { CartDTO } from "@medusajs/framework/types"
+import { CustomerDTO } from "@medusajs/framework/types"
+
+export type SendAbandonedCartsWorkflowInput = {
+ carts: (CartDTO & {
+ customer: CustomerDTO
+ })[]
+}
+
+export const sendAbandonedCartsWorkflow = createWorkflow(
+ "send-abandoned-carts",
+ function (input: SendAbandonedCartsWorkflowInput) {
+ sendAbandonedNotificationsStep(input)
+
+ const updateCartsData = transform(
+ input,
+ (data) => {
+ return data.carts.map((cart) => ({
+ id: cart.id,
+ metadata: {
+ ...cart.metadata,
+ abandoned_notification: new Date().toISOString(),
+ },
+ }))
+ }
+ )
+
+ const updatedCarts = updateCartsStep(updateCartsData)
+
+ return new WorkflowResponse(updatedCarts)
+ }
+)
+```
+
+You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter.
+
+It accepts as a second parameter a constructor function, which is the workflow's implementation. The function can accept input, which in this case is an arra of carts.
+
+In the workflow's constructor function, you:
+
+- Use the `sendAbandonedNotificationsStep` to send the notifications to the carts' customers.
+- Use the `updateCartsStep` from Medusa's core flows to update the carts' metadata with the last notification date.
+
+Notice that you use the `transform` function to prepare the `updateCartsStep`'s input. Medusa does not support direct data manipulation in a workflow's constructor function. You can learn more about it in the [Data Manipulation in Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md).
+
+Your workflow is now ready for use. You will learn how to execute it in the next section.
+
+### Setup Email Template
+
+Before you can test the workflow, you need to set up an email template in SendGrid. The template should contain the dynamic content that you pass in the workflow's step.
+
+To create an email template in SendGrid:
+
+- Go to [Dynamic Templates](https://mc.sendgrid.com/dynamic-templates) in the SendGrid dashboard.
+- Click on the "Create Dynamic Template" button.
+
+
+
+- In the side window that opens, enter a name for the template, then click on the Create button.
+- The template will be added to the middle of the page. When you click on it, a new section will show with an "Add Version" button. Click on it.
+
+
+
+In the form that opens, you can either choose to start with a blank template or from an existing design. You can then use the drag-and-drop or code editor to design the email template.
+
+You can also use the following template as an example:
+
+```html title="Abandoned Cart Email Template"
+
+
+
+
+
+ Complete Your Purchase
+
+
+
+
+
Hi {{customer.first_name}}, your cart is waiting! 🛍️
+
You left some great items in your cart. Complete your purchase before they're gone!
+
+
+```
+
+This template will show each item's image, title, quantity, and price in the cart. It will also show a button to return to the cart and checkout.
+
+You can replace `https://yourstore.com` with your storefront's URL. You'll later implement the `/cart/recover/:cart_id` route in the storefront to recover the cart.
+
+Once you are done, copy the template ID from SendGrid and set it as an environment variable in your Medusa project:
+
+```plain
+ABANDONED_CART_TEMPLATE_ID=your-sendgrid-template-id
+```
+
+***
+
+## Step 4: Schedule Cart Abandonment Notifications
+
+The next step is to automate sending the abandoned cart notifications. You need a task that runs once a day to find the carts that have been abandoned for a certain period and send the notifications to the customers.
+
+To run a task at a scheduled interval, you can use a [scheduled job](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md). A scheduled job is an asynchronous function that the Medusa application runs at the interval you specify during the Medusa application's runtime.
+
+You can create a scheduled job in a TypeScript or JavaScript file under the `src/jobs` directory. So, to create the scheduled job that sends the abandoned cart notifications, create the file `src/jobs/send-abandoned-cart-notification.ts` with the following content:
+
+```ts title="src/jobs/send-abandoned-cart-notification.ts"
+import { MedusaContainer } from "@medusajs/framework/types"
+import {
+ sendAbandonedCartsWorkflow,
+ SendAbandonedCartsWorkflowInput,
+} from "../workflows/send-abandoned-carts"
+
+export default async function abandonedCartJob(
+ container: MedusaContainer
+) {
+ const logger = container.resolve("logger")
+ const query = container.resolve("query")
+
+ const oneDayAgo = new Date()
+ oneDayAgo.setDate(oneDayAgo.getDate() - 1)
+ const limit = 100
+ const offset = 0
+ const totalCount = 0
+ const abandonedCartsCount = 0
+
+ do {
+ // TODO retrieve paginated abandoned carts
+ } while (offset < totalCount)
+
+ logger.info(`Sent ${abandonedCartsCount} abandoned cart notifications`)
+}
+
+export const config = {
+ name: "abandoned-cart-notification",
+ schedule: "0 0 * * *", // Run at midnight every day
+}
+```
+
+In a scheduled job's file, you must export:
+
+1. An asynchronous function that holds the job's logic. The function receives the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) as a parameter.
+2. A `config` object that specifies the job's name and schedule. The schedule is a [cron expression](https://crontab.guru/) that defines the interval at which the job runs.
+
+In the scheduled job function, so far you resolve the [Logger](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md) to log messages, and [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to retrieve data across modules.
+
+You also define a `oneDayAgo` date, which is the date that you will use as the condition of an abandoned cart. In addition, you define variables to paginate the carts.
+
+Next, you will retrieve the abandoned carts using Query. Replace the `TODO` with the following:
+
+```ts title="src/jobs/send-abandoned-cart-notification.ts"
+const {
+ data: abandonedCarts,
+ metadata,
+} = await query.graph({
+ entity: "cart",
+ fields: [
+ "id",
+ "email",
+ "items.*",
+ "metadata",
+ "customer.*",
+ ],
+ filters: {
+ updated_at: {
+ $lt: oneDayAgo,
+ },
+ // @ts-ignore
+ email: {
+ $ne: null,
+ },
+ // @ts-ignore
+ completed_at: null,
+ },
+ pagination: {
+ skip: offset,
+ take: limit,
+ },
+})
+
+totalCount = metadata?.count ?? 0
+const cartsWithItems = abandonedCarts.filter((cart) =>
+ cart.items?.length > 0 && !cart.metadata?.abandoned_notification
+)
+
+try {
+ await sendAbandonedCartsWorkflow(container).run({
+ input: {
+ carts: cartsWithItems,
+ } as unknown as SendAbandonedCartsWorkflowInput,
+ })
+ abandonedCartsCount += cartsWithItems.length
+
+} catch (error) {
+ logger.error(
+ `Failed to send abandoned cart notification: ${error.message}`
+ )
+}
+
+offset += limit
+```
+
+In the do-while loop, you use Query to retrieve carts matching the following criteria:
+
+- The cart was last updated more than a day ago.
+- The cart has an email address.
+- The cart has not been completed.
+
+You also filter the retrieved carts to only include carts with items and customers that have not received an abandoned cart notification.
+
+Finally, you execute the `sendAbandonedCartsWorkflow` passing it the abandoned carts as an input. You will execute the workflow for each paginated batch of carts.
+
+### Test it Out
+
+To test out the scheduled job and workflow, it is recommended to change the `oneDayAgo` date to a minute before now for easy testing:
+
+```ts title="src/jobs/send-abandoned-cart-notification.ts"
+oneDayAgo.setMinutes(oneDayAgo.getMinutes() - 1) // For testing
+```
+
+And to change the job's schedule in `config` to run every minute:
+
+```ts title="src/jobs/send-abandoned-cart-notification.ts"
+export const config = {
+ // ...
+ schedule: "* * * * *", // Run every minute for testing
+}
+```
+
+Finally, start the Medusa application with the following command:
+
+```bash npm2yarn
+npm run dev
+```
+
+And in the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md)'s directory (that you installed in the first step), start the storefront with the following command:
+
+```bash npm2yarn
+npm run dev
+```
+
+Open the storefront at `localhost:8000`. You can either:
+
+- Create an account and add items to the cart, then leave the cart for a minute.
+- Add an item to the cart as a guest. Then, start the checkout process, but only enter the shipping and email addresses, and leave the cart for a minute.
+
+Afterwards, wait for the job to execute. Once it is executed, you will see the following message in the terminal:
+
+```bash
+info: Sent 1 abandoned cart notifications
+```
+
+Once you're done testing, make sure to revert the changes to the `oneDayAgo` date and the job's schedule.
+
+***
+
+## Step 5: Recover Cart in Storefront
+
+In the storefront, you need to add a route that recovers the cart when the customer clicks on the link in the email. The route should receive the cart ID, set the cart ID in the cookie, and redirect the customer to the cart page.
+
+To implement the route, in the Next.js Starter Storefront create the file `src/app/[countryCode]/(main)/cart/recover/[id]/route.tsx` with the following content:
+
+```tsx title="src/app/[countryCode]/(main)/cart/recover/[id]/route.tsx" badgeLabel="Storefront" badgeColor="blue"
+import { NextRequest } from "next/server"
+import { retrieveCart } from "../../../../../../lib/data/cart"
+import { setCartId } from "../../../../../../lib/data/cookies"
+import { notFound, redirect } from "next/navigation"
+type Params = Promise<{
+ id: string
+}>
+
+export async function GET(req: NextRequest, { params }: { params: Params }) {
+ const { id } = await params
+ const cart = await retrieveCart(id)
+
+ if (!cart) {
+ return notFound()
+ }
+
+ setCartId(id)
+
+ const countryCode = cart.shipping_address?.country_code ||
+ cart.region?.countries?.[0]?.iso_2
+
+ redirect(
+ `/${countryCode ? `${countryCode}/` : ""}cart`
+ )
+}
+```
+
+You add a `GET` route handler that receives the cart ID as a path parameter. In the route handler, you:
+
+- Try to retrieve the cart from the Medusa application. The `retrieveCart` function is already available in the Next.js Starter Storefront. If the cart is not found, you return a 404 response.
+- Set the cart ID in a cookie using the `setCartId` function. This is also a function that is already available in the storefront.
+- Redirect the customer to the cart page. You set the country code in the URL based on the cart's shipping address or region.
+
+### Test it Out
+
+To test it out, start the Medusa application:
+
+```bash npm2yarn
+npm run dev
+```
+
+And in the Next.js Starter Storefront's directory, start the storefront:
+
+```bash npm2yarn
+npm run dev
+```
+
+Then, either open the link in an abandoned cart email or navigate to `localhost:8000/cart/recover/:cart_id` in your browser. You will be redirected to the cart page with the recovered cart.
+
+
+
+***
+
+## Next Steps
+
+You have now implemented the logic to send abandoned cart notifications in Medusa. You can implement other customizations with Medusa, such as:
+
+- [Implement Product Reviews](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/how-to-tutorials/tutorials/product-reviews/index.html.md).
+- [Implement Wishlist](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/plugins/guides/wishlist/index.html.md).
+- [Allow Custom-Item Pricing](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/examples/guides/custom-item-price/index.html.md).
+
+If you are new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you will get a more in-depth learning of all the concepts you have used in this guide and more.
+
+To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).
+
+
+# Implement Quick Re-Order Functionality in Medusa
+
+In this tutorial, you'll learn how to implement a re-order functionality in Medusa.
+
+When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md) which are available out-of-the-box. The features include order-management features.
+
+The Medusa Framework facilitates building custom features that are necessary for your business use case. In this tutorial, you'll learn how to implement a re-order functionality in Medusa. This feature is useful for businesses whose customers are likely to repeat their orders, such as B2B or food delivery businesses.
+
+You can follow this guide whether you're new to Medusa or an advanced Medusa developer.
+
+## Summary
+
+By following this tutorial, you'll learn how to:
+
+- Install and set up Medusa.
+- Define the logic to re-order an order.
+- Customize the Next.js Starter Storefront to add a re-order button.
+
+
+
+- [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.
***
@@ -42838,1746 +43623,1307 @@ 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.
+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. Afterward, you can log in with the new user and explore the dashboard.
+Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. 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 Loyalty Module
+## Step 2: Implement Re-Order Workflow
-In Medusa, you can build custom features in a [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). A module is a reusable package with functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup.
+To build custom commerce features in Medusa, you create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). A workflow is a series of queries and actions, called steps, that complete a task.
-In the module, you define the data models necessary for a feature and the logic to manage these data models. Later, you can build commerce flows around your module.
+By using workflows, you can track their executions' progress, define roll-back logic, and configure other advanced features. Then, you execute the workflow from other customizations, such as in an API Route.
-In this step, you'll build a Loyalty Module that defines the necessary data models to store and manage loyalty points for customers.
+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 [Modules documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) to learn more.
-
-### Create Module Directory
-
-Modules are created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/loyalty`.
-
-### Create Data Models
-
-A data model represents a table in the database. You create data models using Medusa's Data Model Language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations.
-
-Refer to the [Data Models documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#1-create-data-model/index.html.md) to learn more.
-
-For the Loyalty Module, you need to define a `LoyaltyPoint` data model that represents a customer's loyalty points. So, create the file `src/modules/loyalty/models/loyalty-point.ts` with the following content:
-
-```ts title="src/modules/loyalty/models/loyalty-point.ts" highlights={dmlHighlights}
-import { model } from "@medusajs/framework/utils"
-
-const LoyaltyPoint = model.define("loyalty_point", {
- id: model.id().primaryKey(),
- points: model.number().default(0),
- customer_id: model.text().unique("IDX_LOYALTY_CUSTOMER_ID"),
-})
-
-export default LoyaltyPoint
-```
-
-You define the `LoyaltyPoint` data model using the `model.define` method of the DML. It accepts the data model's table name as a first parameter, and the model's schema object as a second parameter.
-
-The `LoyaltyPoint` data model has the following properties:
-
-- `id`: A unique ID for the loyalty points.
-- `points`: The number of loyalty points a customer has.
-- `customer_id`: The ID of the customer who owns the loyalty points. This property has a unique index to ensure that each customer has only one record in the `loyalty_point` table.
-
-Learn more about defining data model properties in the [Property Types documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties/index.html.md).
-
-### Create Module's Service
-
-You now have the necessary data model in the Loyalty Module, but you'll need to manage its records. You do this by creating a service in the module.
-
-A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to the database, allowing you to manage your data models, or connect to a third-party service, which is useful if you're integrating with external services.
-
-Refer to the [Module Service documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#2-create-service/index.html.md) to learn more.
-
-To create the Loyalty Module's service, create the file `src/modules/loyalty/service.ts` with the following content:
-
-```ts title="src/modules/loyalty/service.ts"
-import { MedusaError, MedusaService } from "@medusajs/framework/utils"
-import LoyaltyPoint from "./models/loyalty-point"
-import { InferTypeOf } from "@medusajs/framework/types"
-
-type LoyaltyPoint = InferTypeOf
-
-class LoyaltyModuleService extends MedusaService({
- LoyaltyPoint,
-}) {
- // TODO add methods
-}
-
-export default LoyaltyModuleService
-```
-
-The `LoyaltyModuleService` extends `MedusaService` from the Modules SDK which generates a class with data-management methods for your module's data models. This saves you time on implementing Create, Read, Update, and Delete (CRUD) methods.
-
-So, the `LoyaltyModuleService` class now has methods like `createLoyaltyPoints` and `retrieveLoyaltyPoint`.
-
-Find all methods generated by the `MedusaService` in [the Service Factory reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/index.html.md).
-
-#### Add Methods to the Service
-
-Aside from the basic CRUD methods, you need to add methods that handle custom functionalities related to loyalty points.
-
-First, you need a method that adds loyalty points for a customer. Add the following method to the `LoyaltyModuleService`:
-
-```ts title="src/modules/loyalty/service.ts"
-class LoyaltyModuleService extends MedusaService({
- LoyaltyPoint,
-}) {
- async addPoints(customerId: string, points: number): Promise {
- const existingPoints = await this.listLoyaltyPoints({
- customer_id: customerId,
- })
-
- if (existingPoints.length > 0) {
- return await this.updateLoyaltyPoints({
- id: existingPoints[0].id,
- points: existingPoints[0].points + points,
- })
- }
-
- return await this.createLoyaltyPoints({
- customer_id: customerId,
- points,
- })
- }
-}
-```
-
-You add an `addPoints` method that accepts two parameters: the ID of the customer and the points to add.
-
-In the method, you retrieve the customer's existing loyalty points using the `listLoyaltyPoints` method, which is automatically generated by the `MedusaService`. If the customer has existing points, you update them with the new points using the `updateLoyaltyPoints` method.
-
-Otherwise, if the customer doesn't have existing loyalty points, you create a new record with the `createLoyaltyPoints` method.
-
-The next method you'll add deducts points from the customer's loyalty points, which is useful when the customer redeems points. Add the following method to the `LoyaltyModuleService`:
-
-```ts title="src/modules/loyalty/service.ts"
-class LoyaltyModuleService extends MedusaService({
- LoyaltyPoint,
-}) {
- // ...
- async deductPoints(customerId: string, points: number): Promise {
- const existingPoints = await this.listLoyaltyPoints({
- customer_id: customerId,
- })
-
- if (existingPoints.length === 0 || existingPoints[0].points < points) {
- throw new MedusaError(
- MedusaError.Types.NOT_ALLOWED,
- "Insufficient loyalty points"
- )
- }
-
- return await this.updateLoyaltyPoints({
- id: existingPoints[0].id,
- points: existingPoints[0].points - points,
- })
- }
-}
-```
-
-The `deductPoints` method accepts the customer ID and the points to deduct.
-
-In the method, you retrieve the customer's existing loyalty points using the `listLoyaltyPoints` method. If the customer doesn't have existing points or if the points to deduct are greater than the existing points, you throw an error.
-
-Otherwise, you update the customer's loyalty points with the new value using the `updateLoyaltyPoints` method, which is automatically generated by `MedusaService`.
-
-Next, you'll add the method that retrieves the points of a customer. Add the following method to the `LoyaltyModuleService`:
-
-```ts title="src/modules/loyalty/service.ts"
-class LoyaltyModuleService extends MedusaService({
- LoyaltyPoint,
-}) {
- // ...
- async getPoints(customerId: string): Promise {
- const points = await this.listLoyaltyPoints({
- customer_id: customerId,
- })
-
- return points[0]?.points || 0
- }
-}
-```
-
-The `getPoints` method accepts the customer ID and retrieves the customer's loyalty points using the `listLoyaltyPoints` method. If the customer has no points, it returns `0`.
-
-#### Add Method to Map Points to Discount
-
-Finally, you'll add a method that implements the logic of mapping loyalty points to a discount amount. This is useful when the customer wants to redeem their points during checkout.
-
-The mapping logic may differ for each use case. For example, you may need to use a third-party service to map the loyalty points discount amount, or use some custom calculation.
-
-To simplify the logic in this tutorial, you'll use a simple calculation that maps 1 point to 1 currency unit. For example, `100` points = `$100` discount.
-
-Add the following method to the `LoyaltyModuleService`:
-
-```ts title="src/modules/loyalty/service.ts"
-class LoyaltyModuleService extends MedusaService({
- LoyaltyPoint,
-}) {
- // ...
- async calculatePointsFromAmount(amount: number): Promise {
- // Convert amount to points using a standard conversion rate
- // For example, $1 = 1 point
- // Round down to nearest whole point
- const points = Math.floor(amount)
-
- if (points < 0) {
- throw new MedusaError(
- MedusaError.Types.INVALID_DATA,
- "Amount cannot be negative"
- )
- }
-
- return points
- }
-}
-```
-
-The `calculatePointsFromAmount` method accepts the amount and converts it to the nearest whole number of points. If the amount is negative, it throws an error.
-
-You'll use this method later to calculate the amount discounted when a customer redeems their loyalty points.
-
-### Export Module Definition
-
-The final piece to a module is its definition, which you export in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its service.
-
-So, create the file `src/modules/loyalty/index.ts` with the following content:
-
-```ts title="src/modules/loyalty/index.ts"
-import { Module } from "@medusajs/framework/utils"
-import LoyaltyModuleService from "./service"
-
-export const LOYALTY_MODULE = "loyalty"
-
-export default Module(LOYALTY_MODULE, {
- service: LoyaltyModuleService,
-})
-```
-
-You use the `Module` function from the Modules SDK to create the module's definition. It accepts two parameters:
-
-1. The module's name, which is `loyalty`.
-2. An object with a required property `service` indicating the module's service.
-
-You also export the module's name as `LOYALTY_MODULE` so you can reference it later.
-
-### Add Module to Medusa's Configurations
-
-Once you finish building the module, add it to Medusa's configurations to start using it.
-
-In `medusa-config.ts`, add a `modules` property and pass an array with your custom module:
-
-```ts title="medusa-config.ts"
-module.exports = defineConfig({
- // ...
- modules: [
- {
- resolve: "./src/modules/loyalty",
- },
- ],
-})
-```
-
-Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package’s name.
-
-### Generate Migrations
-
-Since data models represent tables in the database, you define how they're created in the database with migrations. A migration is a TypeScript or JavaScript file that defines database changes made by a module.
-
-Refer to the [Migrations documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#5-generate-migrations/index.html.md) to learn more.
-
-Medusa's CLI tool can generate the migrations for you. To generate a migration for the Loyalty Module, run the following command in your Medusa application's directory:
-
-```bash
-npx medusa db:generate loyalty
-```
-
-The `db:generate` command of the Medusa CLI accepts the name of the module to generate the migration for. You'll now have a `migrations` directory under `src/modules/loyalty` that holds the generated migration.
-
-Then, to reflect these migrations on the database, run the following command:
-
-```bash
-npx medusa db:migrate
-```
-
-The table for the `LoyaltyPoint` data model is now created in the database.
-
-***
-
-## Step 3: Change Loyalty Points Flow
-
-Now that you have a module that stores and manages loyalty points in the database, you'll start building flows around it that allow customers to earn and redeem points.
-
-The first flow you'll build will either add points to a customer's loyalty points or deduct them based on a purchased order. If the customer hasn't redeemed points, the points are added to their loyalty points. Otherwise, the points are deducted from their loyalty points.
-
-To build custom commerce features in Medusa, you create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow like you construct a function, but it's a special function that allows you to track its executions' progress, define roll-back logic, and configure other advanced features. Then, you execute the workflow from other customizations, such as in an endpoint.
-
-In this section, you'll build the workflow that adds or deducts loyalty points for an order's customer. Later, you'll execute this workflow when an order is placed.
-
-Learn more about workflows in the [Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).
+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.
-- [validateCustomerExistsStep](#validateCustomerExistsStep): Validate that the customer is registered.
-- [getCartLoyaltyPromoStep](#getCartLoyaltyPromoStep): Retrieve the cart's loyalty promotion.
+- [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.
-Medusa provides the `useQueryGraphStep` and `updatePromotionsStep` in its `@medusajs/medusa/core-flows` package. So, you'll only implement the other steps.
+This workflow uses steps from Medusa's `@medusajs/medusa/core-flows` package. So, you can implement the workflow without implementing custom steps.
-### validateCustomerExistsStep
+### a. Create the Workflow
-In the workflow, you first need to validate that the customer is registered. Only registered customers can earn and redeem loyalty points.
+To create the workflow, create the file `src/workflows/reorder.ts` with the following content:
-To do this, create the file `src/workflows/steps/validate-customer-exists.ts` with the following content:
-
-```ts title="src/workflows/steps/validate-customer-exists.ts"
-import { CustomerDTO } from "@medusajs/framework/types"
-import { createStep } from "@medusajs/framework/workflows-sdk"
-import { MedusaError } from "@medusajs/framework/utils"
-
-export type ValidateCustomerExistsStepInput = {
- customer: CustomerDTO | null | undefined
-}
-
-export const validateCustomerExistsStep = createStep(
- "validate-customer-exists",
- async ({ customer }: ValidateCustomerExistsStepInput) => {
- if (!customer) {
- throw new MedusaError(
- MedusaError.Types.INVALID_DATA,
- "Customer not found"
- )
- }
-
- if (!customer.has_account) {
- throw new MedusaError(
- MedusaError.Types.INVALID_DATA,
- "Customer must have an account to earn or manage points"
- )
- }
- }
-)
-```
-
-You create a step with `createStep` from the Workflows SDK. It accepts two parameters:
-
-1. The step's unique name, which is `validate-customer-exists`.
-2. An async function that receives two parameters:
- - The step's input, which is in this case an object with the customer's details.
- - An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step.
-
-In the step function, you validate that the customer is defined and that it's registered based on its `has_account` property. Otherwise, you throw an error.
-
-### getCartLoyaltyPromoStep
-
-Next, you'll need to retrieve the loyalty promotion applied on the cart, if there's any. This is useful to determine whether the customer has redeemed points.
-
-Before you create a step, you'll create a utility function that the step uses to retrieve the loyalty promotion of a cart. You'll create it as a separate utility function to use it later in other customizations.
-
-Create the file `src/utils/promo.ts` with the following content:
-
-```ts title="src/utils/promo.ts"
-import { PromotionDTO, CustomerDTO, CartDTO } from "@medusajs/framework/types"
-
-export type CartData = CartDTO & {
- promotions?: PromotionDTO[]
- customer?: CustomerDTO
- metadata: {
- loyalty_promo_id?: string
- }
-}
-
-export function getCartLoyaltyPromotion(
- cart: CartData
-): PromotionDTO | undefined {
- if (!cart?.metadata?.loyalty_promo_id) {
- return
- }
-
- return cart.promotions?.find(
- (promotion) => promotion.id === cart.metadata.loyalty_promo_id
- )
-}
-```
-
-You create a `getCartLoyaltyPromotion` function that accepts the cart's details as an input and returns the loyalty promotion if it exists. You retrieve the loyalty promotion if its ID is stored in the cart's `metadata.loyalty_promo_id` property.
-
-You can now create the step that uses this utility to retrieve a carts loyalty points promotion. To create the step, create the file `src/workflows/steps/get-cart-loyalty-promo.ts` with the following content:
-
-```ts title="src/workflows/steps/get-cart-loyalty-promo.ts"
-import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
-import { CartData, getCartLoyaltyPromotion } from "../../utils/promo"
-import { MedusaError } from "@medusajs/framework/utils"
-
-type GetCartLoyaltyPromoStepInput = {
- cart: CartData,
- throwErrorOn?: "found" | "not-found"
-}
-
-export const getCartLoyaltyPromoStep = createStep(
- "get-cart-loyalty-promo",
- async ({ cart, throwErrorOn }: GetCartLoyaltyPromoStepInput) => {
- const loyaltyPromo = getCartLoyaltyPromotion(cart)
-
- if (throwErrorOn === "found" && loyaltyPromo) {
- throw new MedusaError(
- MedusaError.Types.INVALID_DATA,
- "Loyalty promotion already applied to cart"
- )
- } else if (throwErrorOn === "not-found" && !loyaltyPromo) {
- throw new MedusaError(
- MedusaError.Types.INVALID_DATA,
- "No loyalty promotion found on cart"
- )
- }
-
- return new StepResponse(loyaltyPromo)
- }
-)
-```
-
-You create a step that accepts an object having the following properties:
-
-- `cart`: The cart's details.
-- `throwErrorOn`: An optional property that indicates whether to throw an error if the loyalty promotion is found or not found.
-
-The `throwErrorOn` property is useful to make the step reusable in different scenarios, allowing you to use it in later workflows.
-
-In the step, you call the `getCartLoyaltyPromotion` utility to retrieve the loyalty promotion. If the `throwErrorOn` property is set to `found` and the loyalty promotion is found, you throw an error.
-
-Otherwise, if the `throwErrorOn` property is set to `not-found` and the loyalty promotion is not found, you throw an error.
-
-To return data from a step, you return an instance of `StepResponse` from the Workflows SDK. It accepts as a parameter the data to return, which is the loyalty promotion in this case.
-
-### deductPurchasePointsStep
-
-If the order's cart has a loyalty promotion, you need to deduct points from the customer's loyalty points. To do this, create the file `src/workflows/steps/deduct-purchase-points.ts` with the following content:
-
-```ts title="src/workflows/steps/deduct-purchase-points.ts" highlights={deductStepHighlights} collapsibleLines="1-7" expandButtonLabel="Show Imports"
+```ts title="src/workflows/reorder.ts" highlights={workflowHighlights1}
import {
- createStep,
- StepResponse,
+ createWorkflow,
+ transform,
+ WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
-import { LOYALTY_MODULE } from "../../modules/loyalty"
-import LoyaltyModuleService from "../../modules/loyalty/service"
+import {
+ addShippingMethodToCartWorkflow,
+ createCartWorkflow,
+ useQueryGraphStep,
+} from "@medusajs/medusa/core-flows"
-type DeductPurchasePointsInput = {
- customer_id: string
- amount: number
-}
-
-export const deductPurchasePointsStep = createStep(
- "deduct-purchase-points",
- async ({
- customer_id, amount,
- }: DeductPurchasePointsInput, { container }) => {
- const loyaltyModuleService: LoyaltyModuleService = container.resolve(
- LOYALTY_MODULE
- )
-
- const pointsToDeduct = await loyaltyModuleService.calculatePointsFromAmount(
- amount
- )
-
- const result = await loyaltyModuleService.deductPoints(
- customer_id,
- pointsToDeduct
- )
-
- return new StepResponse(result, {
- customer_id,
- points: pointsToDeduct,
- })
- },
- async (data, { container }) => {
- if (!data) {
- return
- }
-
- const loyaltyModuleService: LoyaltyModuleService = container.resolve(
- LOYALTY_MODULE
- )
-
- // Restore points in case of failure
- await loyaltyModuleService.addPoints(
- data.customer_id,
- data.points
- )
- }
-)
-```
-
-You create a step that accepts an object having the following properties:
-
-- `customer_id`: The ID of the customer to deduct points from.
-- `amount`: The promotion's amount, which will be used to calculate the points to deduct.
-
-In the step, you resolve the Loyalty Module's service from the Medusa container. Then, you use the `calculatePointsFromAmount` method to calculate the points to deduct from the promotion's amount.
-
-After that, you call the `deductPoints` method to deduct the points from the customer's loyalty points.
-
-Finally, you return a `StepResponse` with the result of the `deductPoints`.
-
-#### Compensation Function
-
-This step has a compensation function, which is passed as a third parameter to the `createStep` function.
-
-The compensation function undoes the actions performed in a step. Then, if an error occurs during the workflow's execution, the compensation functions of executed steps are called to roll back the changes. This mechanism ensures data consistency in your application, especially as you integrate external systems.
-
-The compensation function accepts two parameters:
-
-1. Data passed from the step function to the compensation function. The data is passed as a second parameter of the returned `StepResponse` instance.
-2. An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md).
-
-In the compensation function, you resolve the Loyalty Module's service from the Medusa container. Then, you call the `addPoints` method to restore the points deducted from the customer's loyalty points if an error occurs.
-
-### addPurchaseAsPointsStep
-
-The last step you'll create adds points to the customer's loyalty points. You'll use this step if the customer didn't redeem points during checkout.
-
-To create the step, create the file `src/workflows/steps/add-purchase-as-points.ts` with the following content:
-
-```ts title="src/workflows/steps/add-purchase-as-points.ts" highlights={addPointsHighlights} collapsibleLines="1-7" expandButtonLabel="Show Imports"
-import {
- createStep,
- StepResponse,
-} from "@medusajs/framework/workflows-sdk"
-import { LOYALTY_MODULE } from "../../modules/loyalty"
-import LoyaltyModuleService from "../../modules/loyalty/service"
-
-type StepInput = {
- customer_id: string
- amount: number
-}
-
-export const addPurchaseAsPointsStep = createStep(
- "add-purchase-as-points",
- async (input: StepInput, { container }) => {
- const loyaltyModuleService: LoyaltyModuleService = container.resolve(
- LOYALTY_MODULE
- )
-
- const pointsToAdd = await loyaltyModuleService.calculatePointsFromAmount(
- input.amount
- )
-
- const result = await loyaltyModuleService.addPoints(
- input.customer_id,
- pointsToAdd
- )
-
- return new StepResponse(result, {
- customer_id: input.customer_id,
- points: pointsToAdd,
- })
- },
- async (data, { container }) => {
- if (!data) {
- return
- }
-
- const loyaltyModuleService: LoyaltyModuleService = container.resolve(
- LOYALTY_MODULE
- )
-
- await loyaltyModuleService.deductPoints(
- data.customer_id,
- data.points
- )
- }
-)
-```
-
-You create a step that accepts an object having the following properties:
-
-- `customer_id`: The ID of the customer to add points to.
-- `amount`: The order's amount, which will be used to calculate the points to add.
-
-In the step, you resolve the Loyalty Module's service from the Medusa container. Then, you use the `calculatePointsFromAmount` method to calculate the points to add from the order's amount.
-
-After that, you call the `addPoints` method to add the points to the customer's loyalty points.
-
-Finally, you return a `StepResponse` with the result of the `addPoints`.
-
-You also pass to the compensation function the customer's ID and the points added. In the compensation function, you deduct the points if an error occurs.
-
-### Add Utility Functions
-
-Before you create the workflow, you need a utility function that checks whether an order's cart has a loyalty promotion. This is useful to determine whether the customer redeemed points during checkout, allowing you to decide which steps to execute.
-
-To add the utility function, add the following to `src/utils/promo.ts`:
-
-```ts title="src/utils/promo.ts"
-import { OrderDTO } from "@medusajs/framework/types"
-
-export type OrderData = OrderDTO & {
- promotion?: PromotionDTO[]
- customer?: CustomerDTO
- cart?: CartData
-}
-
-export const CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE = "customer_id"
-
-export function orderHasLoyaltyPromotion(order: OrderData): boolean {
- const loyaltyPromotion = getCartLoyaltyPromotion(
- order.cart as unknown as CartData
- )
-
- return loyaltyPromotion?.rules?.some((rule) => {
- return rule?.attribute === CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE && (
- rule?.values?.some((value) => value.value === order.customer?.id) || false
- )
- }) || false
-}
-```
-
-You first define an `OrderData` type that extends the `OrderDTO` type. This type has the order's details, including the cart, customer, and promotions details.
-
-Then, you define a constant `CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE` that represents the attribute used in the promotion rule to check whether the customer ID is valid.
-
-Finally, you create the `orderHasLoyaltyPromotion` function that accepts an order's details and checks whether it has a loyalty promotion. It returns `true` if:
-
-- The order's cart has a loyalty promotion. You use the `getCartLoyaltyPromotion` utility to try to retrieve the loyalty promotion.
-- The promotion's rules include the `customer_id` attribute and its value matches the order's customer ID.
- - When you create the promotion for the cart later, you'll see how to set this rule.
-
-You'll use this utility in the workflow next.
-
-### Create the Workflow
-
-Now that you have all the steps, you can create the workflow that uses them.
-
-To create the workflow, create the file `src/workflows/handle-order-points.ts` with the following content:
-
-```ts title="src/workflows/handle-order-points.ts" highlights={handleOrderPointsHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports"
-import { createWorkflow, when } from "@medusajs/framework/workflows-sdk"
-import { updatePromotionsStep, useQueryGraphStep } from "@medusajs/medusa/core-flows"
-import { validateCustomerExistsStep, ValidateCustomerExistsStepInput } from "./steps/validate-customer-exists"
-import { deductPurchasePointsStep } from "./steps/deduct-purchase-points"
-import { addPurchaseAsPointsStep } from "./steps/add-purchase-as-points"
-import { OrderData, CartData } from "../utils/promo"
-import { orderHasLoyaltyPromotion } from "../utils/promo"
-import { getCartLoyaltyPromoStep } from "./steps/get-cart-loyalty-promo"
-
-type WorkflowInput = {
+type ReorderWorkflowInput = {
order_id: string
}
-export const handleOrderPointsWorkflow = createWorkflow(
- "handle-order-points",
- ({ order_id }: WorkflowInput) => {
+export const reorderWorkflow = createWorkflow(
+ "reorder",
+ ({ order_id }: ReorderWorkflowInput) => {
// @ts-ignore
const { data: orders } = useQueryGraphStep({
entity: "order",
fields: [
- "id",
- "customer.*",
- "total",
- "cart.*",
- "cart.promotions.*",
- "cart.promotions.rules.*",
- "cart.promotions.rules.values.*",
- "cart.promotions.application_method.*",
+ "*",
+ "items.*",
+ "shipping_address.*",
+ "billing_address.*",
+ "region.*",
+ "sales_channel.*",
+ "shipping_methods.*",
+ "customer.*",
],
filters: {
id: order_id,
},
- options: {
- throwIfKeyNotFound: true,
- },
})
- validateCustomerExistsStep({
- customer: orders[0].customer,
- } as ValidateCustomerExistsStepInput)
-
- const loyaltyPointsPromotion = getCartLoyaltyPromoStep({
- cart: orders[0].cart as unknown as CartData,
- })
-
- when(orders, (orders) =>
- orderHasLoyaltyPromotion(orders[0] as unknown as OrderData) &&
- loyaltyPointsPromotion !== undefined
- )
- .then(() => {
- deductPurchasePointsStep({
- customer_id: orders[0].customer!.id,
- amount: loyaltyPointsPromotion.application_method!.value as number,
- })
-
- updatePromotionsStep([
- {
- id: loyaltyPointsPromotion.id,
- status: "inactive",
- },
- ])
- })
-
-
- when(
- orders,
- (order) => !orderHasLoyaltyPromotion(order[0] as unknown as OrderData)
- )
- .then(() => {
- addPurchaseAsPointsStep({
- customer_id: orders[0].customer!.id,
- amount: orders[0].total,
- })
- })
+ // 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 with the order's ID.
+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, you:
+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).
-- Use `useQueryGraphStep` to retrieve the order's details. You pass the order's ID as a filter to retrieve the order.
- - This step uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), which is a tool that retrieves data across modules.
-- Validate that the customer is registered using the `validateCustomerExistsStep`.
-- Retrieve the cart's loyalty promotion using the `getCartLoyaltyPromoStep`.
-- Use `when` to check whether the order's cart has a loyalty promotion.
- - Since you can't perform data manipulation in a workflow's constructor function, `when` allows you to perform steps if a condition is satisfied.
- - You pass as a first parameter the object to perform the condition on, which is the order in this case. In the second parameter, you pass a function that returns a boolean value, indicating whether the condition is satisfied.
- - To specify the steps to perform if a condition is satisfied, you chain a `then` method to the `when` method. You can perform any step within the `then` method.
- - In this case, if the order's cart has a loyalty promotion, you call the `deductPurchasePointsStep` to deduct points from the customer's loyalty points. You also call the `updatePromotionsStep` to deactivate the cart's loyalty promotion.
-- You use another `when` to check whether the order's cart doesn't have a loyalty promotion.
- - If the condition is satisfied, you call the `addPurchaseAsPointsStep` to add points to the customer's loyalty points.
+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.
-You'll use this workflow next when an order is placed.
+### b. Create a Cart
-To learn more about the constraints on a workflow's constructor function, refer to the [Workflow Constraints](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md) documentation. Refer to the [When-Then](https://docs.medusajs.com/docs/learn/fundamentals/workflows/conditions/index.html.md) documentation to learn more about the `when` method and how to use it in a workflow.
+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 4: Handle Order Placed Event
+## Step 3: Create Re-Order API Route
-Now that you have the workflow that handles adding or deducting loyalty points for an order, you need to execute it when an order is placed.
+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).
-Medusa has an event system that allows you to listen to events emitted by the Medusa server using a [subscriber](https://docs.medusajs.com/docs//learn/fundamentals/events-and-subscribers/index.html.md). A subscriber is an asynchronous function that's executed when its associated event is emitted. In a subscriber, you can execute a workflow that performs actions in result of the event.
+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.
-In this step, you'll create a subscriber that listens to the `order.placed` event and executes the `handleOrderPointsWorkflow` workflow.
+Refer to the [API Routes documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) to learn more.
-Refer to the [Events and Subscribers](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) documentation 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`.
-Subscribers are created in a TypeScript or JavaScript file under the `src/subscribers` directory. So, to create a subscriber, create the fle `src/subscribers/order-placed.ts` with the following content:
+So, create the file `src/api/store/customers/me/orders/[id]/route.ts` with the following content:
-```ts title="src/subscribers/order-placed.ts"
-import type {
- SubscriberArgs,
- SubscriberConfig,
-} from "@medusajs/framework"
-import { handleOrderPointsWorkflow } from "../workflows/handle-order-points"
+```ts title="src/api/store/customers/me/orders/[id]/route.ts"
+import {
+ AuthenticatedMedusaRequest,
+ MedusaResponse,
+} from "@medusajs/framework/http"
+import { reorderWorkflow } from "../../../../../../workflows/reorder"
-export default async function orderPlacedHandler({
- event: { data },
- container,
-}: SubscriberArgs<{ id: string }>) {
- await handleOrderPointsWorkflow(container).run({
+export async function POST(
+ req: AuthenticatedMedusaRequest,
+ res: MedusaResponse
+) {
+ const { id } = req.params
+
+ const { result } = await reorderWorkflow(req.scope).run({
input: {
- order_id: data.id,
+ order_id: id,
},
})
-}
-export const config: SubscriberConfig = {
- event: "order.placed",
+ return res.json({
+ cart: result,
+ })
}
```
-The subscriber file must export:
+Since you export a `POST` route handler function, you expose a `POST` API route at `/store/customers/me/orders/:id`.
-- An asynchronous subscriber function that's executed whenever the associated event, which is `order.placed` is triggered.
-- A configuration object with an event property whose value is the event the subscriber is listening to. You can also pass an array of event names to listen to multiple events in the same subscriber.
+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 subscriber function accepts an object with the following properties:
+The route handler function accepts two parameters:
-- `event`: An object with the event's data payload. For example, the `order.placed` event has the order's ID in its data payload.
-- `container`: The Medusa container, which you can use to resolve services and tools.
+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 subscriber function, you execute the `handleOrderPointsWorkflow` by invoking it, passing it the Medusa container, then using its `run` method, passing it the workflow's input.
+In the route handler function, you execute the `reorderWorkflow`. To execute a workflow, you:
-Whenever an order is placed now, the subscriber will be executed, which in turn will execute the workflow that handles the loyalty points flow.
+- 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
-To test out the loyalty points flow, you'll use the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) that you installed in the first step. As mentioned in that step, the storefront will be installed in a separate directory from the Medusa application, and its name is `{project-name}-storefront`, where `{project-name}` is the name of your Medusa application's directory.
+You'll now test out the re-order functionality.
-So, run the following command in the Medusa application's directory to start the Medusa server:
+First, to start the Medusa application, run the following command in the Medusa application's directory:
-```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green"
+```bash npm2yarn badgeLabel="Medusa application" badgeColor="green"
npm run dev
```
-Then, run the following command in the Next.js Starter Storefront's directory to start the Next.js server:
+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 Next.js Starter Storefront will be running on `http://localhost:8000`, and the Medusa server will be running on `http://localhost:9000`.
+The storefront will be running at `http://localhost:8000`. Open it in your browser.
-Open the Next.js Starter Storefront in your browser and create a new account by going to Account at the top right.
+To test out the re-order functionality:
-Once you're logged in, add an item to the cart and go through the checkout flow.
+- 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.
-After you place the order, you'll see the following message in your Medusa application's terminal:
+
-```bash
-info: Processing order.placed which has 1 subscribers
-```
+On the order's details page, you'll find a "Reorder" button.
-This message indicates that the `order.placed` event was emitted, and that your subscriber was executed.
+
-Since you didn't redeem any points during checkout, loyalty points will be added to your account. You'll implement an API route that allows you to retrieve the loyalty points in the next step.
+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.
-***
-
-## Step 5: Retrieve Loyalty Points API Route
-
-Next, you want to allow customers to view their loyalty points. You can show them on their profile page, or during checkout.
-
-To expose a feature to clients, you create an [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). An API Route is an endpoint that exposes commerce features to external applications and clients, such as storefronts.
-
-You'll create an API route at the path `/store/customers/me/loyalty-points` that returns the loyalty points of the authenticated customer.
-
-Learn more about API routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md).
-
-An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`.
-
-So, to create an API route at the path `/store/customers/me/loyalty-points`, create the file `src/api/store/customers/me/loyalty-points/route.ts` with the following content:
-
-```ts title="src/api/store/customers/me/loyalty-points/route.ts"
-
-import {
- AuthenticatedMedusaRequest,
- MedusaResponse,
-} from "@medusajs/framework/http"
-import { LOYALTY_MODULE } from "../../../../../modules/loyalty"
-import LoyaltyModuleService from "../../../../../modules/loyalty/service"
-
-export async function GET(
- req: AuthenticatedMedusaRequest,
- res: MedusaResponse
-) {
- const loyaltyModuleService: LoyaltyModuleService = req.scope.resolve(
- LOYALTY_MODULE
- )
-
- const points = await loyaltyModuleService.getPoints(
- req.auth_context.actor_id
- )
-
- res.json({
- points,
- })
-}
-```
-
-Since you export a `GET` route handler function, you're exposing a `GET` endpoint at `/store/customers/me/loyalty-points`. The route handler function accepts two parameters:
-
-1. A request object with details and context on the request, such as body parameters or authenticated customer details.
-2. A response object to manipulate and send the response.
-
-In the route handler, you resolve the Loyalty Module's service from the Medusa container (which is available at `req.scope`).
-
-Then, you call the service's `getPoints` method to retrieve the authenticated customer's loyalty points. Note that routes starting with `/store/customers/me` are only accessible by authenticated customers. You can access the authenticated customer ID from the request's context, which is available at `req.auth_context.actor_id`.
-
-Finally, you return the loyalty points in the response.
-
-You'll test out this route as you customize the Next.js Starter Storefront next.
-
-***
-
-## Step 6: Show Loyalty Points During Checkout
-
-Now that you have the API route to retrieve the loyalty points, you can show them during checkout.
-
-In this step, you'll customize the Next.js Starter Storefront to show the loyalty points in the checkout page.
-
-First, you'll add a server action function that retrieves the loyalty points from the route you created earlier. In `src/lib/data/customer.ts`, add the following function:
-
-```ts title="src/lib/data/customer.ts" badgeLabel="Storefront" badgeColor="blue"
-export const getLoyaltyPoints = async () => {
- const headers = {
- ...(await getAuthHeaders()),
- }
-
- return sdk.client.fetch<{ points: number }>(
- `/store/customers/me/loyalty-points`,
- {
- method: "GET",
- headers,
- }
- )
- .then(({ points }) => points)
- .catch(() => null)
-}
-```
-
-You add a `getLoyaltyPoints` function that retrieves the authenticated customer's loyalty points from the API route you created earlier. You pass the authentication headers using the `getAuthHeaders` function, which is a utility function defined in the Next.js Starter Storefront.
-
-If the customer isn't authenticated, the request will fail. So, you catch the error and return `null` in that case.
-
-Next, you'll create a component that shows the loyalty points in the checkout page. Create the file `src/modules/checkout/components/loyalty-points/index.tsx` with the following content:
-
-```tsx title="src/modules/checkout/components/loyalty-points/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={loyaltyPointsHighlights}
-"use client"
-
-import { HttpTypes } from "@medusajs/types"
-import { useEffect, useMemo, useState } from "react"
-import { getLoyaltyPoints } from "../../../../lib/data/customer"
-import { Button, Heading } from "@medusajs/ui"
-import Link from "next/link"
-
-type LoyaltyPointsProps = {
- cart: HttpTypes.StoreCart & {
- promotions: HttpTypes.StorePromotion[]
- }
-}
-
-const LoyaltyPoints = ({ cart }: LoyaltyPointsProps) => {
- const isLoyaltyPointsPromoApplied = useMemo(() => {
- return cart.promotions.find(
- (promo) => promo.id === cart.metadata?.loyalty_promo_id
- ) !== undefined
- }, [cart])
- const [loyaltyPoints, setLoyaltyPoints] = useState<
- number | null
- >(null)
-
- useEffect(() => {
- getLoyaltyPoints()
- .then((points) => {
- console.log(points)
- setLoyaltyPoints(points)
- })
- }, [])
-
- const handleTogglePromotion = async (
- e: React.MouseEvent
- ) => {
- e.preventDefault()
- // TODO apply or remove loyalty promotion
- }
-
- return (
- <>
-
-
-
- Loyalty Points
-
- {loyaltyPoints === null && (
-
- Sign up to get and use loyalty points
-
- )}
- {loyaltyPoints !== null && (
-
-
-
- You have {loyaltyPoints} loyalty points
-
-
- )}
-
- >
- )
-}
-
-export default LoyaltyPoints
-```
-
-You create a `LoyaltyPoints` component that accepts the cart's details as a prop. In the component, you:
-
-- Create a `isLoyaltyPointsPromoApplied` memoized value that checks whether the cart has a loyalty promotion applied. You use the `cart.metadata.loyalty_promo_id` property to check this.
-- Create a `loyaltyPoints` state to store the customer's loyalty points.
-- Call the `getLoyaltyPoints` function in a `useEffect` hook to retrieve the loyalty points from the API route you created earlier. You set the `loyaltyPoints` state with the retrieved points.
-- Define `handleTogglePromotion` that, when clicked, would either apply or remove the promotion. You'll implement these functionalities later.
-- Render the loyalty points in the component. If the customer isn't authenticated, you show a link to the account page to sign up. Otherwise, you show the loyalty points and a button to apply or remove the promotion.
-
-Next, you'll show this component at the end of the checkout's summary component. So, import the component in `src/modules/checkout/templates/checkout-summary/index.tsx`:
-
-```tsx title="src/modules/checkout/templates/checkout-summary/index.tsx" badgeLabel="Storefront" badgeColor="blue"
-import LoyaltyPoints from "../../components/loyalty-points"
-```
-
-Then, in the return statement of the `CheckoutSummary` component, add the following after the `div` wrapping the `DiscountCode`:
-
-```tsx title="src/modules/checkout/templates/checkout-summary/index.tsx" badgeLabel="Storefront" badgeColor="blue"
-
-```
-
-This will show the loyalty points component at the end of the checkout summary.
-
-### Test it Out
-
-To test out the customizations to the checkout flow, make sure both the Medusa application and Next.js Starter Storefront are running.
-
-Then, as an authenticated customer, add an item to cart and proceed to checkout. You'll find a new "Loyalty Points" section at the end of the checkout summary.
-
-
-
-If you made a purchase before, you can see your loyalty points. You'll also see the "Apply Loyalty Points" button, which doesn't yet do anything. You'll add the functionality next.
-
-***
-
-## Step 7: Apply Loyalty Points to Cart
-
-The next feature you'll implement allows the customer to apply their loyalty points during checkout. To implement the feature, you need:
-
-- A workflow that implements the steps of the apply loyalty points flow.
-- An API route that exposes the workflow's functionality to clients. You'll then send a request to this API route to apply the loyalty points on the customer's cart.
-- A function in the Next.js Starter Storefront that sends the request to the API route you created earlier.
-
-The workflow will have the following steps:
-
-- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details.
-- [validateCustomerExistsStep](#validateCustomerExistsStep): Validate that the customer is registered.
-- [getCartLoyaltyPromoStep](#getCartLoyaltyPromoStep): Retrieve the cart's loyalty promotion.
-- [getCartLoyaltyPromoAmountStep](#getCartLoyaltyPromoAmountStep): Get the amount to be discounted based on the loyalty points.
-- [createPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPromotionsStep/index.html.md): Create a new loyalty promotion for the cart.
-- [updateCartPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/workflows/updateCartPromotionsWorkflow/index.html.md): Update the cart's promotions with the new loyalty promotion.
-- [updateCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartsStep/index.html.md): Update the cart to store the ID of the loyalty promotion in the metadata.
-- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details again.
-
-Most of the workflow's steps are either provided by Medusa in the `@medusajs/medusa/core-flows` package or steps you've already implemented. You only need to implement the `getCartLoyaltyPromoAmountStep` step.
-
-### getCartLoyaltyPromoAmountStep
-
-The fourth step in the workflow is the `getCartLoyaltyPromoAmountStep`, which retrieves the amount to be discounted based on the loyalty points. This step is useful to determine how much discount to apply to the cart.
-
-To create the step, create the file `src/workflows/steps/get-cart-loyalty-promo-amount.ts` with the following content:
-
-```ts title="src/workflows/steps/get-cart-loyalty-promo-amount.ts" highlights={getCartLoyaltyPromoAmountStepHighlights}
-import { PromotionDTO, CustomerDTO } from "@medusajs/framework/types"
-import { MedusaError } from "@medusajs/framework/utils"
-import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
-import LoyaltyModuleService from "../../modules/loyalty/service"
-import { LOYALTY_MODULE } from "../../modules/loyalty"
-
-export type GetCartLoyaltyPromoAmountStepInput = {
- cart: {
- id: string
- customer: CustomerDTO
- promotions?: PromotionDTO[]
- total: number
- }
-}
-
-export const getCartLoyaltyPromoAmountStep = createStep(
- "get-cart-loyalty-promo-amount",
- async ({ cart }: GetCartLoyaltyPromoAmountStepInput, { container }) => {
- // Check if customer has any loyalty points
- const loyaltyModuleService: LoyaltyModuleService = container.resolve(
- LOYALTY_MODULE
- )
- const loyaltyPoints = await loyaltyModuleService.getPoints(
- cart.customer.id
- )
-
- if (loyaltyPoints <= 0) {
- throw new MedusaError(
- MedusaError.Types.INVALID_DATA,
- "Customer has no loyalty points"
- )
- }
-
- const pointsAmount = await loyaltyModuleService.calculatePointsFromAmount(
- loyaltyPoints
- )
-
- const amount = Math.min(pointsAmount, cart.total)
-
- return new StepResponse(amount)
- }
-)
-```
-
-You create a step that accepts an object having the cart's details.
-
-In the step, you resolve the Loyalty Module's service from the Medusa container. Then, you call the `getPoints` method to retrieve the customer's loyalty points. If the customer has no loyalty points, you throw an error.
-
-Next, you call the `calculatePointsFromAmount` method to calculate the amount to be discounted based on the loyalty points. You use the `Math.min` function to ensure that the amount doesn't exceed the cart's total.
-
-Finally, you return a `StepResponse` with the amount to be discounted.
-
-### Create the Workflow
-
-You can now create the workflow that applies a loyalty promotion to the cart.
-
-To create the workflow, create the file `src/workflows/apply-loyalty-on-cart.ts` with the following content:
-
-```ts title="src/workflows/apply-loyalty-on-cart.ts" highlights={applyLoyaltyOnCartWorkflowHighlights} collapsibleLines="1-24" expandButtonLabel="Show Imports"
-import {
- createWorkflow,
- transform,
- WorkflowResponse,
-} from "@medusajs/framework/workflows-sdk"
-import {
- createPromotionsStep,
- updateCartPromotionsWorkflow,
- updateCartsStep,
- useQueryGraphStep,
-} from "@medusajs/medusa/core-flows"
-import {
- validateCustomerExistsStep,
- ValidateCustomerExistsStepInput,
-} from "./steps/validate-customer-exists"
-import {
- getCartLoyaltyPromoAmountStep,
- GetCartLoyaltyPromoAmountStepInput,
-} from "./steps/get-cart-loyalty-promo-amount"
-import { CartData, CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE } from "../utils/promo"
-import { CreatePromotionDTO } from "@medusajs/framework/types"
-import { PromotionActions } from "@medusajs/framework/utils"
-import { getCartLoyaltyPromoStep } from "./steps/get-cart-loyalty-promo"
-
-type WorkflowInput = {
- cart_id: string
-}
-
-const fields = [
- "id",
- "customer.*",
- "promotions.*",
- "promotions.application_method.*",
- "promotions.rules.*",
- "promotions.rules.values.*",
- "currency_code",
- "total",
- "metadata",
-]
-
-export const applyLoyaltyOnCartWorkflow = createWorkflow(
- "apply-loyalty-on-cart",
- (input: WorkflowInput) => {
- // @ts-ignore
- const { data: carts } = useQueryGraphStep({
- entity: "cart",
- fields,
- filters: {
- id: input.cart_id,
- },
- options: {
- throwIfKeyNotFound: true,
- },
- })
-
- validateCustomerExistsStep({
- customer: carts[0].customer,
- } as ValidateCustomerExistsStepInput)
-
- getCartLoyaltyPromoStep({
- cart: carts[0] as unknown as CartData,
- throwErrorOn: "found",
- })
-
- const amount = getCartLoyaltyPromoAmountStep({
- cart: carts[0],
- } as unknown as GetCartLoyaltyPromoAmountStepInput)
-
- // TODO create and apply the promotion on the cart
- }
-)
-```
-
-You create a workflow that accepts an object with the cart's ID as input.
-
-So far, you:
-
-- Use `useQueryGraphStep` to retrieve the cart's details. You pass the cart's ID as a filter to retrieve the cart.
-- Validate that the customer is registered using the `validateCustomerExistsStep`.
-- Check whether the cart has a loyalty promotion using the `getCartLoyaltyPromoStep`. You pass the `throwErrorOn` parameter with the value `found` to throw an error if a loyalty promotion is found in the cart.
-- Retrieve the amount to be discounted based on the loyalty points using the `getCartLoyaltyPromoAmountStep`.
-
-Next, you need to create a new loyalty promotion for the cart. First, you'll prepare the data of the promotion to be created.
-
-Replace the `TODO` with the following:
-
-```ts title="src/workflows/apply-loyalty-on-cart.ts" highlights={prepareLoyaltyPromoDataHighlights}
-const promoToCreate = transform({
- carts,
- amount,
-}, (data) => {
- const randomStr = Math.random().toString(36).substring(2, 8)
- const uniqueId = (
- "LOYALTY-" + data.carts[0].customer?.first_name + "-" + randomStr
- ).toUpperCase()
- return {
- code: uniqueId,
- type: "standard",
- status: "active",
- application_method: {
- type: "fixed",
- value: data.amount,
- target_type: "order",
- currency_code: data.carts[0].currency_code,
- allocation: "across",
- },
- rules: [
- {
- attribute: CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE,
- operator: "eq",
- values: [data.carts[0].customer!.id],
- },
- ],
- campaign: {
- name: uniqueId,
- description: "Loyalty points promotion for " + data.carts[0].customer!.email,
- campaign_identifier: uniqueId,
- budget: {
- type: "usage",
- limit: 1,
- },
- },
- }
-})
-
-// TODO create promotion and apply it on cart
-```
-
-Since data manipulation isn't allowed in a workflow constructor, you use the [transform](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) function from the Workflows SDK. It accepts two parameters:
-
-- The data to perform manipulation on. In this case, you pass the cart's details and the amount to be discounted.
-- A function that receives the data from the first parameter, and returns the transformed data.
-
-In the transformation function, you prepare th data of the loyalty promotion to be created. Some key details include:
-
-- You set the discount amount in the application method of the promotion.
-- You add a rule to the promotion that ensures it can be used only in carts having their `customer_id` equal to this customer's ID. This prevents other customers from using this promotion.
-- You create a campaign for the promotion, and you set the campaign budget to a single usage. This prevents the customer from using the promotion again.
-
-Learn more about promotion concepts in the [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md)'s documentation.
-
-You can now use the returned data to create a promotion and apply it to the cart. Replace the new `TODO` with the following:
-
-```ts title="src/workflows/apply-loyalty-on-cart.ts" highlights={createLoyaltyPromoStepHighlights}
-const loyaltyPromo = createPromotionsStep([
- promoToCreate,
-] as CreatePromotionDTO[])
-
-const { metadata, ...updatePromoData } = transform({
- carts,
- promoToCreate,
- loyaltyPromo,
-}, (data) => {
- const promos = [
- ...(data.carts[0].promotions?.map((promo) => promo?.code).filter(Boolean) || []) as string[],
- data.promoToCreate.code,
- ]
-
- return {
- cart_id: data.carts[0].id,
- promo_codes: promos,
- action: PromotionActions.ADD,
- metadata: {
- loyalty_promo_id: data.loyaltyPromo[0].id,
- },
- }
-})
-
-updateCartPromotionsWorkflow.runAsStep({
- input: updatePromoData,
-})
-
-updateCartsStep([
- {
- id: input.cart_id,
- metadata,
- },
-])
-
-// retrieve cart with updated promotions
-// @ts-ignore
-const { data: updatedCarts } = useQueryGraphStep({
- entity: "cart",
- fields,
- filters: { id: input.cart_id },
-}).config({ name: "retrieve-cart" })
-
-return new WorkflowResponse(updatedCarts[0])
-```
-
-In the rest of the workflow, you:
-
-- Create the loyalty promotion using the data you prepared earlier using the `createPromotionsStep`.
-- Use the `transform` function to prepare the data to update the cart's promotions. You add the new loyalty promotion code to the cart's promotions codes, and set the `loyalty_promo_id` in the cart's metadata.
-- Update the cart's promotions with the new loyalty promotion using the `updateCartPromotionsWorkflow` workflow.
-- Update the cart's metadata with the loyalty promotion ID using the `updateCartsStep`.
-- Retrieve the cart's details again using `useQueryGraphStep` to get the updated cart with the new loyalty promotion.
-
-To return data from the workflow, you must return an instance of `WorkflowResponse`. You pass it the data to be returned, which is in this case the cart's details.
-
-### Create the API Route
-
-Next, you'll create the API route that executes this workflow.
-
-To create the API route, create the file `src/api/store/carts/[id]/loyalty-points/route.ts` with the following content:
-
-```ts title="src/api/store/carts/[id]/loyalty-points/route.ts"
-import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
-import { applyLoyaltyOnCartWorkflow } from "../../../../../workflows/apply-loyalty-on-cart"
-
-export async function POST(
- req: MedusaRequest,
- res: MedusaResponse
-) {
- const { id: cart_id } = req.params
-
- const { result: cart } = await applyLoyaltyOnCartWorkflow(req.scope)
- .run({
- input: {
- cart_id,
- },
- })
-
- res.json({ cart })
-}
-```
-
-Since you export a `POST` route handler, you expose a `POST` API route at `/store/carts/[id]/loyalty-points`.
-
-In the route handler, you execute the `applyLoyaltyOnCartWorkflow` workflow, passing it the cart ID as an input. You return the cart's details in the response.
-
-You can now use this API route in the Next.js Starter Storefront.
-
-### Apply Loyalty Points in the Storefront
-
-In the Next.js Starter Storefront, you need to add a server action function that sends a request to the API route you created earlier. Then, you'll use that function when the customer clicks the "Apply Loyalty Points" button.
-
-To add the function, add the following to `src/lib/data/cart.ts` in the Next.js Starter Storefront:
-
-```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="blue"
-export async function applyLoyaltyPointsOnCart() {
- const cartId = await getCartId()
- const headers = {
- ...(await getAuthHeaders()),
- }
-
- return await sdk.client.fetch<{
- cart: HttpTypes.StoreCart & {
- promotions: HttpTypes.StorePromotion[]
- }
- }>(`/store/carts/${cartId}/loyalty-points`, {
- method: "POST",
- headers,
- })
- .then(async (result) => {
- const cartCacheTag = await getCacheTag("carts")
- revalidateTag(cartCacheTag)
-
- return result
- })
-}
-```
-
-You create an `applyLoyaltyPointsOnCart` function that sends a request to the API route you created earlier.
-
-In the function, you retrieve the cart ID stored in the cookie using the `getCartId` function, which is available in the Next.js Starter Storefront.
-
-Then, you send the request. Once the request is resolved successfully, you revalidate the cart cache tag to ensure that the cart's details are updated and refetched by other components. This ensures that the applied promotion is shown in the checkout summary without needing to refresh the page.
-
-Finally, you'll use this function in the `handleTogglePromotion` function in the `LoyaltyPoints` component you created earlier.
-
-At the top of `src/modules/checkout/components/loyalty-points/index.tsx`, import the function:
-
-```tsx title="src/modules/checkout/components/loyalty-points/index.tsx" badgeLabel="Storefront" badgeColor="blue"
-import { applyLoyaltyPointsOnCart } from "../../../../lib/data/cart"
-```
-
-Then, replace the `handleTogglePromotion` function with the following:
-
-```tsx title="src/modules/checkout/components/loyalty-points/index.tsx" badgeLabel="Storefront" badgeColor="blue"
-const handleTogglePromotion = async (
- e: React.MouseEvent
-) => {
- e.preventDefault()
- if (!isLoyaltyPointsPromoApplied) {
- await applyLoyaltyPointsOnCart()
- } else {
- // TODO remove loyalty points
- }
-}
-```
-
-In the `handleTogglePromotion` function, you call the `applyLoyaltyPointsOnCart` function if the cart doesn't have a loyalty promotion. This will send a request to the API route you created earlier, which will execute the workflow that applies the loyalty promotion to the cart.
-
-You'll implement removing the loyalty points promotion in a later step.
-
-### Test it Out
-
-To test out applying the loyalty points on the cart, start the Medusa application and Next.js Starter Storefront.
-
-Then, in the checkout flow as an authenticated customer, click on the "Apply Loyalty Points" button. The checkout summary will be updated with the applied promotion and the discount amount.
-
-If you don't want the promotion to be shown in the "Promotions(s) applied" section, you can filter the promotions in `src/modules/checkout/components/discount-code/index.tsx` to not show a promotion matching `cart.metadata.loyalty_promo_id`.
-
-
-
-***
-
-## Step 8: Remove Loyalty Points From Cart
-
-In this step, you'll implement the functionality to remove the loyalty points promotion from the cart. This is useful if the customer changes their mind and wants to remove the promotion.
-
-To implement this functionality, you'll need to:
-
-- Create a workflow that removes the loyalty points promotion from the cart.
-- Create an API route that executes the workflow.
-- Create a function in the Next.js Starter Storefront that sends a request to the API route you created earlier.
-- Use the function in the `handleTogglePromotion` function in the `LoyaltyPoints` component you created earlier.
-
-### Create the Workflow
-
-The workflow will have the following steps:
-
-- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details.
-- [getCartLoyaltyPromoStep](#getCartLoyaltyPromoStep): Retrieve the cart's loyalty promotion.
-- [updateCartPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/workflows/updateCartPromotionsWorkflow/index.html.md): Update the cart's promotions to remove the loyalty promotion.
-- [updateCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartsStep/index.html.md): Update the cart to remove the loyalty promotion ID from the metadata.
-- [updatePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePromotionsStep/index.html.md): Deactive the loyalty promotion.
-- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details again.
-
-Since you already have all the steps, you can create the workflow.
-
-To create the workflow, create the file `src/workflows/remove-loyalty-from-cart.ts` with the following content:
-
-```ts title="src/workflows/remove-loyalty-from-cart.ts" collapsibleLines="1-15" expandButtonLabel="Show Imports" highlights={removeLoyaltyFromCartWorkflowHighlights}
-import {
- createWorkflow,
- transform,
- WorkflowResponse,
-} from "@medusajs/framework/workflows-sdk"
-import {
- useQueryGraphStep,
- updateCartPromotionsWorkflow,
- updateCartsStep,
- updatePromotionsStep,
-} from "@medusajs/medusa/core-flows"
-import { getCartLoyaltyPromoStep } from "./steps/get-cart-loyalty-promo"
-import { PromotionActions } from "@medusajs/framework/utils"
-import { CartData } from "../utils/promo"
-
-type WorkflowInput = {
- cart_id: string
-}
-
-const fields = [
- "id",
- "customer.*",
- "promotions.*",
- "promotions.application_method.*",
- "promotions.rules.*",
- "promotions.rules.values.*",
- "currency_code",
- "total",
- "metadata",
-]
-
-export const removeLoyaltyFromCartWorkflow = createWorkflow(
- "remove-loyalty-from-cart",
- (input: WorkflowInput) => {
- // @ts-ignore
- const { data: carts } = useQueryGraphStep({
- entity: "cart",
- fields,
- filters: {
- id: input.cart_id,
- },
- })
-
- const loyaltyPromo = getCartLoyaltyPromoStep({
- cart: carts[0] as unknown as CartData,
- throwErrorOn: "not-found",
- })
-
- updateCartPromotionsWorkflow.runAsStep({
- input: {
- cart_id: input.cart_id,
- promo_codes: [loyaltyPromo.code!],
- action: PromotionActions.REMOVE,
- },
- })
-
- const newMetadata = transform({
- carts,
- }, (data) => {
- const { loyalty_promo_id, ...rest } = data.carts[0].metadata || {}
-
- return {
- ...rest,
- loyalty_promo_id: null,
- }
- })
-
- updateCartsStep([
- {
- id: input.cart_id,
- metadata: newMetadata,
- },
- ])
-
- updatePromotionsStep([
- {
- id: loyaltyPromo.id,
- status: "inactive",
- },
- ])
-
- // retrieve cart with updated promotions
- // @ts-ignore
- const { data: updatedCarts } = useQueryGraphStep({
- entity: "cart",
- fields,
- filters: { id: input.cart_id },
- }).config({ name: "retrieve-cart" })
-
- return new WorkflowResponse(updatedCarts[0])
- }
-)
-```
-
-You create a workflow that accepts an object with the cart's ID as input.
-
-In the workflow, you:
-
-- Use `useQueryGraphStep` to retrieve the cart's details. You pass the cart's ID as a filter to retrieve the cart.
-- Check whether the cart has a loyalty promotion using the `getCartLoyaltyPromoStep`. You pass the `throwErrorOn` parameter with the value `not-found` to throw an error if a loyalty promotion isn't found in the cart.
-- Update the cart's promotions using the `updateCartPromotionsWorkflow`, removing the loyalty promotion.
-- Use the `transform` function to prepare the new metadata of the cart. You remove the `loyalty_promo_id` from the metadata.
-- Update the cart's metadata with the new metadata using the `updateCartsStep`.
-- Deactivate the loyalty promotion using the `updatePromotionsStep`.
-- Retrieve the cart's details again using `useQueryGraphStep` to get the updated cart with the new loyalty promotion.
-- Return the cart's details in a `WorkflowResponse` instance.
-
-### Create the API Route
-
-Next, you'll create the API route that executes this workflow.
-
-To create the API route, add the following in `src/api/store/carts/[id]/loyalty-points/route.ts`:
-
-```ts title="src/api/store/carts/[id]/loyalty-points/route.ts"
-// other imports...
-import { removeLoyaltyFromCartWorkflow } from "../../../../../workflows/remove-loyalty-from-cart"
-
-// ...
-export async function DELETE(
- req: MedusaRequest,
- res: MedusaResponse
-) {
- const { id: cart_id } = req.params
-
- const { result: cart } = await removeLoyaltyFromCartWorkflow(req.scope)
- .run({
- input: {
- cart_id,
- },
- })
-
- res.json({ cart })
-}
-```
-
-You export a `DELETE` route handler, which exposes a `DELETE` API route at `/store/carts/[id]/loyalty-points`.
-
-In the route handler, you execute the `removeLoyaltyFromCartWorkflow` workflow, passing it the cart ID as an input. You return the cart's details in the response.
-
-You can now use this API route in the Next.js Starter Storefront.
-
-### Remove Loyalty Points in the Storefront
-
-In the Next.js Starter Storefront, you need to add a server action function that sends a request to the API route you created earlier. Then, you'll use that function when the customer clicks the "Remove Loyalty Points" button, which shows when the cart has a loyalty promotion applied.
-
-To add the function, add the following to `src/lib/data/cart.ts`:
-
-```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="blue"
-export async function removeLoyaltyPointsOnCart() {
- const cartId = await getCartId()
- const headers = {
- ...(await getAuthHeaders()),
- }
- const next = {
- ...(await getCacheOptions("carts")),
- }
-
- return await sdk.client.fetch<{
- cart: HttpTypes.StoreCart & {
- promotions: HttpTypes.StorePromotion[]
- }
- }>(`/store/carts/${cartId}/loyalty-points`, {
- method: "DELETE",
- headers,
- })
- .then(async (result) => {
- const cartCacheTag = await getCacheTag("carts")
- revalidateTag(cartCacheTag)
-
- return result
- })
-}
-```
-
-You create a `removeLoyaltyPointsOnCart` function that sends a request to the API route you created earlier.
-
-In the function, you retrieve the cart ID stored in the cookie using the `getCartId` function, which is available in the Next.js Starter Storefront.
-
-Then, you send the request to the API route. Once the request is resolved successfully, you revalidate the cart cache tag to ensure that the cart's details are updated and refetched by other components. This ensures that the promotion is removed from the checkout summary without needing to refresh the page.
-
-Finally, you'll use this function in the `handleTogglePromotion` function in the `LoyaltyPoints` component you created earlier.
-
-At the top of `src/modules/checkout/components/loyalty-points/index.tsx`, add the following import:
-
-```tsx title="src/modules/checkout/components/loyalty-points/index.tsx" badgeLabel="Storefront" badgeColor="blue"
-import { removeLoyaltyPointsOnCart } from "../../../../lib/data/cart"
-```
-
-Then, replace the `TODO` in `handleTogglePromotion` with the following:
-
-```tsx title="src/modules/checkout/components/loyalty-points/index.tsx" badgeLabel="Storefront" badgeColor="blue"
-await removeLoyaltyPointsOnCart()
-```
-
-In the `handleTogglePromotion` function, you call the `removeLoyaltyPointsOnCart` function if the cart has a loyalty promotion. This will send a request to the API route you created earlier, which will execute the workflow that removes the loyalty promotion from the cart.
-
-### Test it Out
-
-To test out removing the loyalty points from the cart, start the Medusa application and Next.js Starter Storefront.
-
-Then, in the checkout flow as an authenticated customer, after applying the loyalty points, click on the "Remove Loyalty Points" button. The checkout summary will be updated with the removed promotion and the discount amount.
-
-
-
-***
-
-## Step 9: Validate Loyalty Points on Cart Completion
-
-After the customer applies the loyalty points to the cart and places the order, you need to validate that the customer actually has the loyalty points. This prevents edge cases where the customer may have applied the loyalty points previously but they don't have them anymore.
-
-So, in this step, you'll hook into Medusa's cart completion flow to perform the validation.
-
-Since Medusa uses workflows in its API routes, it allows you to hook into them and perform custom functionalities using [Workflow Hooks](https://docs.medusajs.com/docs/learn/fundamentals/workflows/workflow-hooks/index.html.md). A workflow hook is a point in a workflow where you can inject custom functionality as a step function, called a hook handler.
-
-Medusa uses the [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) hook to complete the cart and place an order. This workflow has a `validate` hook that allows you to perform custom validation before the cart is completed.
-
-To consume the `validate` hook, create the file `src/workflows/hooks/complete-cart.ts` with the following content:
-
-```ts title="src/workflows/hooks/complete-cart.ts" highlights={completeCartWorkflowHookHighlights} collapsibleLines="1-6" expandButtonLabel="Show Imports"
-import { completeCartWorkflow } from "@medusajs/medusa/core-flows"
-import LoyaltyModuleService from "../../modules/loyalty/service"
-import { LOYALTY_MODULE } from "../../modules/loyalty"
-import { CartData, getCartLoyaltyPromotion } from "../../utils/promo"
-import { MedusaError } from "@medusajs/framework/utils"
-
-completeCartWorkflow.hooks.validate(
- async ({ cart }, { container }) => {
- const query = container.resolve("query")
- const loyaltyModuleService: LoyaltyModuleService = container.resolve(
- LOYALTY_MODULE
- )
-
- const { data: carts } = await query.graph({
- entity: "cart",
- fields: [
- "id",
- "promotions.*",
- "customer.*",
- "promotions.rules.*",
- "promotions.rules.values.*",
- "promotions.application_method.*",
- "metadata",
- ],
- filters: {
- id: cart.id,
- },
- }, {
- throwIfKeyNotFound: true,
- })
-
- const loyaltyPromo = getCartLoyaltyPromotion(
- carts[0] as unknown as CartData
- )
-
- if (!loyaltyPromo) {
- return
- }
-
- const customerLoyaltyPoints = await loyaltyModuleService.getPoints(
- carts[0].customer!.id
- )
- const requiredPoints = await loyaltyModuleService.calculatePointsFromAmount(
- loyaltyPromo.application_method!.value as number
- )
-
- if (customerLoyaltyPoints < requiredPoints) {
- throw new MedusaError(
- MedusaError.Types.INVALID_DATA,
- `Customer does not have enough loyalty points. Required: ${
- requiredPoints
- }, Available: ${customerLoyaltyPoints}`
- )
- }
- }
-)
-```
-
-Workflows have a special `hooks` property that includes all the hooks tht you can consume in that workflow. You consume the hook by invoking it from the workflow's `hooks` property.
-
-Since the hook is essentially a step function, it accepts the following parameters:
-
-- The hook's input passed from the workflow, which differs for each hook. The `validate` hook receives an object having the cart's details.
-- The step context object, which contains the Medusa container. You can use it to resolve services and perform actions.
-
-In the hook, you resolve Query and the Loyalty Module's service. Then, you use Query to retrieve the cart's necessary details, including its promotions, customer, and metadata.
-
-After that, you retrieve the customer's loyalty points and calculate the required points to apply the loyalty promotion.
-
-If the customer doesn't have enough loyalty points, you throw an error. This will prevent the cart from being completed if the customer doesn't have enough loyalty points.
-
-***
-
-## Test Out Cart Completion with Loyalty Points
-
-Since you now have the entire loyalty points flow implemented, you can test it out by going through the checkout flow, applying the loyalty points to the cart.
-
-When you place the order, if the customer has sufficient loyalty points, the validation hook will pass.
-
-Then, the `order.placed` event will be emitted, which will execute the subscriber that calls the `handleOrderPointsWorkflow`.
-
-In the workflow, since the order's cart has a loyalty promotion, the points equivalent to the promotion will be deducted, and the promotion becomes inactive.
-
-You can confirm that the loyalty points were deducted either by sending a request to the [retrieve loyalty points API route](#step-5-retrieve-loyalty-points-api-route), or by going through the checkout process again in the storefront.
+
***
## Next Steps
-You've now implement a loyalty points system in Medusa. There's still more that you can implement based on your use case:
+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.
-- Add loyalty points on registration or other events. Refer to the [Events Reference](https://docs.medusajs.com/references/events/index.html.md) for a full list of available events you can listen to.
-- Show the customer their loyalty point usage history. This will require adding another data model in the Loyalty Module that records the usage history. You can create records of that data model when an order that has a loyalty promotion is placed, then customize the storefront to show a new page for loyalty points history.
-- Customize the Medusa Admin to show a new page or [UI Route](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md) for loyalty points information and analytics.
+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.
+
+When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md) which are available out-of-the-box.
+
+Medusa's architecture facilitates integrating third-party services, such as payment providers. These payment providers can process payments and securely store customers' payment methods for future use.
+
+In this tutorial, you'll expand on Medusa's [Stripe Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md) to allow customers to re-use their saved payment methods during checkout.
+
+You can follow this guide whether you're new to Medusa or an advanced Medusa developer.
+
+While this tutorial uses Stripe as an example, you can follow the same steps to implement saved payment methods with other payment providers.
+
+## Summary
+
+By following this tutorial, you'll learn how to:
+
+- Install and set up Medusa and the Next.js Starter Storefront.
+- Set up the Stripe Module Provider in Medusa.
+- Customize the checkout flow to save customers' payment methods.
+- Allow customers to select saved payment methods during checkout.
+
+
+
+[Saved Payment Methods Repository](https://github.com/medusajs/examples/tree/main/stripe-saved-payment): Find the full code for this guide in this repository.
+
+***
+
+## Step 1: Install a Medusa Application
+
+### Prerequisites
+
+- [Node.js v20+](https://nodejs.org/en/download)
+- [Git CLI tool](https://git-scm.com/downloads)
+- [PostgreSQL](https://www.postgresql.org/download/)
+
+Start by installing the Medusa application on your machine with the following command:
+
+```bash
+npx create-medusa-app@latest
+```
+
+You'll first be asked for the project's name. Then, when asked whether you want to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose Yes.
+
+Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js Starter Storefront in a separate directory with the `{project-name}-storefront` name.
+
+The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).
+
+Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterwards, you can log in with the new user and explore the dashboard.
+
+Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.
+
+***
+
+## Step 2: Set Up the Stripe Module Provider
+
+Medusa's [Payment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/index.html.md) provides payment-related models and the interface to manage and process payments. However, it delegates the actual payment processing to module providers that integrate third-party payment services.
+
+The [Stripe Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md) is a Payment Module Provider that integrates Stripe into your Medusa application to process payments. It can also save payment methods securely.
+
+In this section, you'll set up the Stripe Module Provider in your Medusa application.
+
+### Prerequisites
+
+- [Stripe account](https://stripe.com/)
+- [Stripe Secret and Public API Keys](https://support.stripe.com/questions/locate-api-keys-in-the-dashboard)
+
+### Register the Stripe Module Provider
+
+To register the Stripe Module Provider in your Medusa application, add it to the array of providers passed to the Payment Module in `medusa-config.ts`:
+
+```ts title="medusa-config.ts"
+module.exports = defineConfig({
+ // ...
+ modules: [
+ {
+ resolve: "@medusajs/medusa/payment",
+ options: {
+ providers: [
+ {
+ resolve: "@medusajs/medusa/payment-stripe",
+ id: "stripe",
+ options: {
+ apiKey: process.env.STRIPE_API_KEY,
+ },
+ },
+ ],
+ },
+ },
+ ],
+})
+```
+
+The Medusa configuration accepts a `modules` array, which contains the modules to be loaded. While the Payment Module is loaded by default, you need to add it again when registering a new provider.
+
+You register provides in the `providers` option of the Payment Module. Each provider is an object with the following properties:
+
+- `resolve`: The package name of the provider.
+- `id`: The ID of the provider. This is used to identify the provider in the Medusa application.
+- `options`: The options to be passed to the provider. In this case, the `apiKey` option is required for the Stripe Module Provider.
+
+Learn about other options in the [Stripe Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe#module-options/index.html.md) documentation.
+
+### Add Environment Variables
+
+Next, add the following environment variables to your `.env` file:
+
+```plain
+STRIPE_API_KEY=sk_...
+```
+
+Where `STRIPE_API_KEY` is your Stripe Secret API Key. You can find it in the Stripe dashboard under Developers > API keys.
+
+
+
+### Enable Stripe in a Region
+
+In Medusa, each [region](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/region/index.html.md) (which is a geographical area where your store operates) can have different payment methods enabled. So, after registering the Stripe Module Provider, you need to enable it in a region.
+
+To enable it in a region, start the Medusa application with the following command:
+
+```bash npm2yarn
+npm run dev
+```
+
+Then, go to `localhost:9000/app` and log in with the user you created earlier.
+
+Once you're logged in:
+
+1. Go to Settings -> Regions.
+2. Click on the region where you want to enable the payment provider.
+3. Click the icon at the top right of the first section
+4. Choose "Edit" from the dropdown menu
+5. In the side window that opens, find the "Payment Providers" field and select Stripe from the dropdown.
+6. Once you're done, click the "Save" button.
+
+Stripe will now be available as a payment option during checkout.
+
+The Stripe Module Provider supports different payment methods in Stripe, such as Bancontact or iDEAL. This guide focuses only on the card payment method, but you can enable other payment methods as well.
+
+
+
+### Add Evnironement Variable to Storefront
+
+The [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) supports payment with Stripe during checkout if it's enabled in the region.
+
+The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.
+
+So, if your Medusa application's directory is `medusa-payment`, you can find the storefront by going back to the parent directory and changing to the `medusa-payment-storefront` directory:
+
+```bash
+cd ../medusa-payment-storefront # change based on your project name
+```
+
+In the Next.js Starter Storefront project, add the Stripe public API key as an environment variable in `.env.local`:
+
+```plain badgeLabel="Storefront" badgeColor="blue"
+NEXT_PUBLIC_STRIPE_KEY=pk_123...
+```
+
+Where `NEXT_PUBLIC_STRIPE_KEY` is your Stripe public API key. You can find it in the Stripe dashboard under Developers > API keys.
+
+***
+
+## Step 3: List Payment Methods API Route
+
+The Payment Module uses [account holders](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/account-holder/index.html.md) to represent a customer's details that are stored in a third-party payment provider. Medusa creates an account holder for each customer, allowing you later to retrieve the customer's saved payment methods in the third-party provider.
+
+
+
+While this feature is available out-of-the-box, you need to expose it to clients, like storefronts, by creating an [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). An API Route is an endpoint that exposes commerce features to external applications and clients.
+
+In this step, you'll create an API route that lists the saved payment methods for an authenticated customer.
+
+Refer to the [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) documentation to learn more.
+
+### Create API Route
+
+An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`, and it can include path parameters using square brackets.
+
+So, to create an API route at the path `/store/payment-methods/:account-holder-id`, create the file `src/api/store/payment-methods/[account_holder_id]/route.ts` with the following content:
+
+```ts title="src/api/store/payment-methods/[account_holder_id]/route.ts" highlights={apiRouteHighlights}
+import { MedusaError } from "@medusajs/framework/utils"
+import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
+
+export async function GET(
+ req: MedusaRequest,
+ res: MedusaResponse
+) {
+ const { account_holder_id } = req.params
+ const query = req.scope.resolve("query")
+ const paymentModuleService = req.scope.resolve("payment")
+
+ const { data: [accountHolder] } = await query.graph({
+ entity: "account_holder",
+ fields: [
+ "data",
+ "provider_id",
+ ],
+ filters: {
+ id: account_holder_id,
+ },
+ })
+
+ if (!accountHolder) {
+ throw new MedusaError(
+ MedusaError.Types.NOT_FOUND,
+ "Account holder not found"
+ )
+ }
+
+ const paymentMethods = await paymentModuleService.listPaymentMethods(
+ {
+ provider_id: accountHolder.provider_id,
+ context: {
+ account_holder: {
+ data: {
+ id: accountHolder.data.id,
+ },
+ },
+ },
+ }
+ )
+
+ res.json({
+ payment_methods: paymentMethods,
+ })
+}
+```
+
+Since you export a route handler function named `GET`, you expose a `GET` API route at `/store/payment-methods/:account-holder-id`. The route handler function accepts two parameters:
+
+1. A request object with details and context on the request, such as body parameters or authenticated customer details.
+2. A response object to manipulate and send the response.
+
+The request object has a `scope` property, which is an instance of the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). The Medusa container is a registry of Framework and commerce tools that you can access in the API route.
+
+You use the Medusa container to resolve:
+
+- [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), which is a tool that retrieves data across modules in the Medusa application.
+- The [Payment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/index.html.md)'s service, which provides an interface to manage and process payments with third-party providers.
+
+You use Query to retrieve the account holder with the ID passed as a path parameter. If the account holder is not found, you throw an error.
+
+Then, you use the [listPaymentMethods](https://docs.medusajs.com/references/payment/listPaymentMethods/index.html.md) method of the Payment Module's service to retrieve the payment providers saved in the third-party provider. The method accepts an object with the following properties:
+
+- `provider_id`: The ID of the provider, such as Stripe's ID. The account holder stores the ID its associated provider.
+- `context`: The context of the request. In this case, you pass the account holder's ID to retrieve the payment methods associated with it in the third-party provider.
+
+Finally, you return the payment methods in the response.
+
+### Protect API Route
+
+Only authenticated customers can access and use saved payment methods. So, you need to protect the API route to ensure that only authenticated customers can access it.
+
+To protect an API route, you can add a [middleware](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md). A middleware is a function executed when a request is sent to an API Route. You can add an authentication middleware that ensures that the request is authenticated before executing the route handler function.
+
+Refer to the [Middlewares](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md) documentation to learn more.
+
+Middlewares are added in the `src/api/middlewares.ts` file. So, create the file with the following content:
+
+```ts title="src/api/middlewares.ts"
+import { authenticate, defineMiddlewares } from "@medusajs/framework/http"
+
+export default defineMiddlewares({
+ routes: [
+ {
+ matcher: "/store/payment-methods/:provider_id/:account_holder_id",
+ method: "GET",
+ middlewares: [
+ authenticate("customer", ["bearer", "session"]),
+ ],
+ },
+ ],
+})
+```
+
+The `src/api/middlewares.ts` file must use the `defineMiddlewares` function and export its result. The `defineMiddlewares` function accepts a `routes` array that accepts objects with the following properties:
+
+- `matcher`: The path of the API route to apply the middleware to.
+- `method`: The HTTP method of the API route to apply the middleware to.
+- `middlewares`: An array of middlewares to apply to the API route.
+
+You apply the `authenticate` middleware to the API route you created earlier. The `authenticate` middleware ensures that only authenticated customers can access the API route.
+
+Refer to the [Protected Routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/protected-routes/index.html.md) documentation to learn more about the `authenticate` middleware.
+
+Your API route can now only be accessed by authenticated customers. You'll test it out as you customize the Next.js Starter Storefront in the next steps.
+
+***
+
+## Step 4: Save Payment Methods During Checkout
+
+In this step, you'll customize the checkout flow in the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) to save payment methods during checkout.
+
+The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.
+
+So, if your Medusa application's directory is `medusa-payment`, you can find the storefront by going back to the parent directory and changing to the `medusa-payment-storefront` directory:
+
+```bash
+cd ../medusa-payment-storefront # change based on your project name
+```
+
+During checkout, when the customer chooses a payment method, such as Stripe, the Next.js Starter Storefront creates a [payment session](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-session/index.html.md) in Medusa using the [Initialize Payment Session](https://docs.medusajs.com/api/store#payment-collections_postpaymentcollectionsidpaymentsessions) API route.
+
+Under the hood, Medusa uses the associated payment provider (Stripe) to initiate the payment process with the associated third-party payment provider. The [Initialize Payment Session](https://docs.medusajs.com/api/store#payment-collections_postpaymentcollectionsidpaymentsessions) API route accepts a `data` object parameter in the request body that allows you to pass data relevant to the third-party payment provider.
+
+So, to save the payment method that the customer uses during checkout with Stripe, you must pass the `setup_future_usage` property in the `data` object. The `setup_future_usage` property is a Stripe-specific property that allows you to save the payment method for future use.
+
+In `src/modules/checkout/components/payment/index.tsx` of the Next.js Starter Storefront, there are two uses of the `initiatePaymentSession` function. Update each of them to pass the `data` property:
+
+```ts title="src/modules/checkout/components/payment/index.tsx" badgeLabel="Storefront" badgeColor="blue"
+// update in two places
+await initiatePaymentSession(cart, {
+ // ...
+ data: {
+ setup_future_usage: "off_session",
+ },
+})
+```
+
+You customize the `initiatePaymentSession` function to pass the `data` object with the `setup_future_usage` property. You set the value to `off_session` to allow using the payment method outside of the checkout flow, such as for follow up payments. You can use `on_session` instead if you only want the payment method to be used by the customer during checkout.
+
+By making this change, you always save the payment method that the customer uses during checkout. You can alternatively show a checkbox to confirm saving the payment method, and only pass the `data` object if the customer checks it.
+
+### Test it Out
+
+To test it out, start the Medusa application by running the following command in the Medusa application's directory:
+
+```bash npm2yarn
+npm run dev
+```
+
+Then, start the Next.js Starter Storefront by running the following command in the storefront's directory:
+
+```bash npm2yarn
+npm run dev
+```
+
+You can open the storefront in your browser at `localhost:8000`. Then, create a new customer account by clicking on the "Account" link at the top right.
+
+After creating an account and logging in, add a product to the cart and go to the checkout page. Once you get to the payment step, choose Stripe and enter a [test card number](https://docs.stripe.com/testing#cards), such as `4242 4242 4242 4242`.
+
+Then, place the order. Once the order is placed, you can check on the Stripe dashboard that the payment method was saved by:
+
+1. Going to the "Customers" section in the Stripe dashboard.
+2. Clicking on the customer you just placed the order with.
+3. Scrolling down to the "Payment methods" section. You'll find the payment method you just used to place the order.
+
+
+
+In the next step, you'll show the saved payment methods to the customer during checkout and allow them to select one of them.
+
+***
+
+## Step 5: Use Saved Payment Methods During Checkout
+
+In this step, you'll customize the checkout flow in the Next.js Starter Storefront to show the saved payment methods to the customer and allow them to select one of them to place the order.
+
+### Retrieve Saved Payment Methods
+
+To retrieve the saved payment methods, you'll add a server function that retrieves the customer's saved payment methods from the API route you created earlier.
+
+Add the following in `src/lib/data/payment.ts`:
+
+```ts title="src/lib/data/payment.ts" badgeLabel="Storefront" badgeColor="blue" highlights={paymentHighlights}
+export type SavedPaymentMethod = {
+ id: string
+ provider_id: string
+ data: {
+ card: {
+ brand: string
+ last4: string
+ exp_month: number
+ exp_year: number
+ }
+ }
+}
+
+export const getSavedPaymentMethods = async (accountHolderId: string) => {
+ const headers = {
+ ...(await getAuthHeaders()),
+ }
+
+ return sdk.client.fetch<{
+ payment_methods: SavedPaymentMethod[]
+ }>(
+ `/store/payment-methods/${accountHolderId}`,
+ {
+ method: "GET",
+ headers,
+ }
+ ).catch(() => {
+ return {
+ payment_methods: [],
+ }
+ })
+}
+```
+
+You define a type for the retrieved payment methods. It contains the following properties:
+
+- `id`: The ID of the payment method in the third-party provider.
+- `provider_id`: The ID of the provider in the Medusa application, such as Stripe's ID.
+- `data`: Additional data retrieved from the third-party provider related to the saved payment method. The type is modeled after the data returned by Stripe, but you can change it to match other payment providers.
+
+You also create a `getSavedPaymentMethods` function that retrieves the saved payment methods from the API route you created earlier. The function accepts the account holder ID as a parameter and returns the saved payment methods.
+
+### Add Saved Payment Methods Component
+
+Next, you'll add the component that shows the saved payment methods and allows the customer to select one of them.
+
+The component that shows the Stripe card element is defined in `src/modules/checkout/components/payment-container/index.tsx`. So, you'll define the component for the saved payment methods in the same file.
+
+Start by adding the following import statements at the top of the file:
+
+```ts title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue"
+import { Button } from "@medusajs/ui"
+import { useEffect, useState } from "react"
+import { HttpTypes } from "@medusajs/types"
+import { SavedPaymentMethod, getSavedPaymentMethods } from "@lib/data/payment"
+import { initiatePaymentSession } from "../../../../lib/data/cart"
+import { capitalize } from "lodash"
+```
+
+Then, update the `PaymentContainerProps` type to include the payment session and cart details:
+
+```ts title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue"
+type PaymentContainerProps = {
+ // ...
+ paymentSession?: HttpTypes.StorePaymentSession
+ cart: HttpTypes.StoreCart
+}
+```
+
+You'll need these details to find which saved payment method the customer selected, and to initiate a new payment session for the cart when the customer chooses a saved payment method.
+
+Next, add the following component at the end of the file:
+
+```tsx title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={stripeSavedPaymentMethodsHighlights}
+const StripeSavedPaymentMethodsContainer = ({
+ paymentSession,
+ setCardComplete,
+ setCardBrand,
+ setError,
+ cart,
+}: {
+ paymentSession?: HttpTypes.StorePaymentSession
+ setCardComplete: (complete: boolean) => void
+ setCardBrand: (brand: string) => void
+ setError: (error: string | null) => void
+ cart: HttpTypes.StoreCart
+}) => {
+ const [savedPaymentMethods, setSavedPaymentMethods] = useState<
+ SavedPaymentMethod[]
+ >([])
+ const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<
+ string | undefined
+ >(
+ paymentSession?.data?.payment_method_id as string | undefined
+ )
+
+ useEffect(() => {
+ const accountHolderId = (
+ paymentSession?.context?.account_holder as Record
+ )
+ ?.id
+
+ if (!accountHolderId) {
+ return
+ }
+
+ getSavedPaymentMethods(accountHolderId)
+ .then(({ payment_methods }) => {
+ setSavedPaymentMethods(payment_methods)
+ })
+ }, [paymentSession])
+
+ useEffect(() => {
+ if (!selectedPaymentMethod || !savedPaymentMethods.length) {
+ setCardComplete(false)
+ setCardBrand("")
+ setError(null)
+ return
+ }
+ const selectedMethod = savedPaymentMethods.find(
+ (method) => method.id === selectedPaymentMethod
+ )
+
+ if (!selectedMethod) {
+ return
+ }
+
+ setCardBrand(capitalize(selectedMethod.data.card.brand))
+ setCardComplete(true)
+ setError(null)
+ }, [selectedPaymentMethod, savedPaymentMethods])
+
+ const handleSelect = async (method: SavedPaymentMethod) => {
+ // initiate a new payment session with the selected payment method
+ await initiatePaymentSession(cart, {
+ provider_id: method.provider_id,
+ data: {
+ payment_method_id: method.id,
+ },
+ }).catch((error) => {
+ setError(error.message)
+ })
+
+ setSelectedPaymentMethod(method.id)
+ }
+
+ if (!savedPaymentMethods.length) {
+ return <>>
+ }
+
+ // TODO add return statement
+}
+```
+
+You define a `StripeSavedPaymentMethodsContainer` component that accepts the following props:
+
+- `paymentSession`: The cart's current payment session.
+- `setCardComplete`: A function to tell parent components whether the cart or payment method selection is complete. This allows the customer to proceed to the next step in the checkout flow.
+- `setCardBrand`: A function to set the brand of the selected payment method. This is useful to show the brand of the selected payment method in review sections of the checkout flow.
+- `setError`: A function to set the error message in case of an error.
+- `cart`: The cart's details.
+
+In the component, you define a state variable to store the saved payment methods and another one to store the selected payment method.
+
+Then, you use the `useEffect` hook to retrieve the saved payment methods for the account holder set in the cart's payment session. You use the `getSavedPaymentMethods` function you created earlier to retrieve the saved payment methods.
+
+You also use another `useEffect` hook that is executed when the selected payment method changes. In this hook, you check if the selected payment method is valid and set the card brand and completion status accordingly.
+
+Finally, you define a `handleSelect` function that you'll execute when the customer selects a saved payment method. It creates a new payment session with the selected payment method.
+
+To show the saved payment methods, replace the `TODO` with the following `return` statement:
+
+```tsx title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue"
+return (
+
+)
+```
+
+You display the saved payment methods as radio buttons. When the customer selects one of them, you execute the `handleSelect` function to initiate a new payment session with the selected payment method.
+
+### Modify Existing Stripe Element
+
+Now that you have the component to show the saved payment methods, you need to modify the existing Stripe element to allow customers to select an existing payment method or enter a new one.
+
+In the same `src/modules/checkout/components/payment-container/index.tsx` file, expand the new `paymentSession` and `cart` props of the `StripeCardContainer` component:
+
+```ts title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue"
+export const StripeCardContainer = ({
+ // ...
+ paymentSession,
+ cart,
+}: Omit & {
+ // ...
+}) => {
+ // ...
+}
+```
+
+Then, add a new state variable that keeps track of whether the customer is using a saved payment method or entering a new one:
+
+```ts title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue"
+const [isUsingSavedPaymentMethod, setIsUsingSavedPaymentMethod] = useState(
+ paymentSession?.data?.payment_method_id !== undefined
+)
+```
+
+Next, add a function that resets the payment session when the customer switches between saved and new payment methods:
+
+```ts title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue"
+const handleRefreshSession = async () => {
+ await initiatePaymentSession(cart, {
+ provider_id: paymentProviderId,
+ })
+ setIsUsingSavedPaymentMethod(false)
+}
+```
+
+This function initiates a new payment session for the cart and disables the `isUsingSavedPaymentMethod` state variable.
+
+Finally, replace the `return` statement of the `StripeCardContainer` component with the following:
+
+```tsx title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={stripeCardReturnHighlights}
+return (
+
+ {selectedPaymentOptionId === paymentProviderId &&
+ (stripeReady ? (
+
+ ) : (
+
+ ))}
+
+)
+```
+
+You update the `return` statement to:
+
+- Pass the new `paymentSession` and `cart` props to the `PaymentContainer` component.
+- Show the `StripeSavedPaymentMethodsContainer` component before Stripe's card element.
+- Add a button that's shown when the customer selects a saved payment method. The button allows the customer to switch back to entering a new payment method.
+
+The existing Stripe element in checkout will now show the saved payment methods to the customer along with the component to enter a card's details.
+
+Since you added new props to the `StripeCardContainer` and `PaymentContainer` components, you need to update other components that use them to pass the props.
+
+In `src/modules/checkout/components/payment/index.tsx`, find usages of `StripeCardContainer` and `PaymentContainer` in the return statement and add the `paymentSession` and `cart` props:
+
+```tsx title="src/modules/checkout/components/payment/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["5"], ["6"], ["11"], ["12"]]}
+
+```
+
+### Support Updating Stripe's Client Secret
+
+The Next.js Starter Storefront uses Stripe's `Elements` component to wrap the payment elements. The `Elements` component requires a `clientSecret` prop, which is available in the cart's payment session.
+
+With the recent changes, the client secret will be updated whenever a payment session is initiated, such as when the customer selects a saved payment method. However, the `options.clientSecret` prop of the `Elements` component is immutable, meaning that it cannot be changed after the component is mounted.
+
+To force the component to re-mount and update the `clientSecret` prop, you can add a `key` prop to the `Elements` component. The `key` prop ensures that the `Elements` component re-mounts whenever the client secret changes, allowing Stripe to process the updated payment session.
+
+In `src/modules/checkout/components/payment-wrapper/stripe-wrapper.tsx`, find the `Elements` component in the `return` statement and add the `key` prop:
+
+```tsx title="src/modules/checkout/components/payment-wrapper/stripe-wrapper.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["4"]]}
+
+ {children}
+
+```
+
+You set the `key` prop to the client secret, which forces the `Elements` component to re-mount whenever the client secret changes.
+
+### Support Payment with Saved Payment Method
+
+The last change you need to make ensures that the customer can place an order with a saved payment method.
+
+When the customer places the order, and they've chosen Stripe as a payment method, the Next.js Starter Storefront uses Stripe's `confirmCardPayment` method to confirm the payment. This method accepts either the ID of a saved payment method, or the details of a new card.
+
+So, you need to update the `confirmCardPayment` usage to support passing the ID of the selected payment method if the customer has selected one.
+
+In `src/modules/checkout/components/payment-button/index.tsx`, find the `handlePayment` method and update its first `if` condition:
+
+```ts title="src/modules/checkout/components/payment-button/index.tsx" badgeLabel="Storefront" badgeColor="blue"
+if (!stripe || !elements || (!card && !session?.data.payment_method_id) || !cart) {
+ setSubmitting(false)
+ return
+}
+```
+
+This allows the customer to place their order if they have selected a saved payment method but have not entered a new card.
+
+Then, find the usage of `confirmCardPayment` in the `handlePayment` function and change it to the following:
+
+```ts title="src/modules/checkout/components/payment-button/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={confirmPaymentHighlights}
+await stripe
+.confirmCardPayment(session?.data.client_secret as string, {
+ payment_method: session?.data.payment_method_id as string || {
+ card: card!,
+ billing_details: {
+ name:
+ cart.billing_address?.first_name +
+ " " +
+ cart.billing_address?.last_name,
+ address: {
+ city: cart.billing_address?.city ?? undefined,
+ country: cart.billing_address?.country_code ?? undefined,
+ line1: cart.billing_address?.address_1 ?? undefined,
+ line2: cart.billing_address?.address_2 ?? undefined,
+ postal_code: cart.billing_address?.postal_code ?? undefined,
+ state: cart.billing_address?.province ?? undefined,
+ },
+ email: cart.email,
+ phone: cart.billing_address?.phone ?? undefined,
+ },
+ },
+})
+.then(({ error, paymentIntent }) => {
+ if (error) {
+ const pi = error.payment_intent
+
+ if (
+ (pi && pi.status === "requires_capture") ||
+ (pi && pi.status === "succeeded")
+ ) {
+ onPaymentCompleted()
+ }
+
+ setErrorMessage(error.message || null)
+ return
+ }
+
+ if (
+ (paymentIntent && paymentIntent.status === "requires_capture") ||
+ paymentIntent.status === "succeeded"
+ ) {
+ return onPaymentCompleted()
+ }
+
+ return
+})
+```
+
+In particular, you're changing the `payment_method` property to either be the ID of the selected payment method, or the details of a new card. This allows the customer to place an order with either a saved payment method or a new one.
+
+### Test it Out
+
+You can now test out placing orders with a saved payment method.
+
+To do that, start the Medusa application by running the following command in the Medusa application's directory:
+
+```bash npm2yarn
+npm run dev
+```
+
+Then, start the Next.js Starter Storefront by running the following command in the storefront's directory:
+
+```bash npm2yarn
+npm run dev
+```
+
+In the Next.js Starter Storefront, login with the customer account you created earlier and add a product to the cart.
+
+Then, proceed to the checkout flow. In the payment step, you should see the saved payment method you used earlier. You can select it and place the order.
+
+
+
+Once the order is placed successfully, you can check it in the Medusa Admin dashboard. You can view the order and capture the payment.
+
+
+
+***
+
+## Next Steps
+
+You've added support for saved payment methods in your Medusa application and Next.js Starter Storefront, allowing customers to save their payment methods during checkout and use them in future orders.
+
+You can add more features to the saved payment methods, such as allowing customers to delete saved payment methods. You can use [Stripe's APIs](https://docs.stripe.com/api/payment_methods/detach) in the storefront or add an [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) in Medusa to delete the saved payment method.
If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth learning of all the concepts you've used in this guide and more.
@@ -46454,1167 +46800,32 @@ 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
+# Implement Loyalty Points System in Medusa
-In this tutorial, you'll learn how to implement a re-order functionality in Medusa.
+In this tutorial, you'll learn how to implement a loyalty points system in Medusa.
-When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md) which are available out-of-the-box. The features include order-management features.
+Medusa Cloud provides a beta Store Credits feature that facilitates building a loyalty point system. [Get in touch](https://medusajs.com/contact) for early access.
-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.
+When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md), which are available out-of-the-box. These features include management capabilities related to carts, orders, promotions, and more.
-You can follow this guide whether you're new to Medusa or an advanced Medusa developer.
+A loyalty point system allows customers to earn points for purchases, which can be redeemed for discounts or rewards. In this tutorial, you'll learn how to customize the Medusa application to implement a loyalty points system.
+
+You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer.
## Summary
-By following this tutorial, you'll learn how to:
+By following this tutorial, you will 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.
+- Define models to store loyalty points and the logic to manage them.
+- Build flows that allow customers to earn and redeem points during checkout.
+ - Points are redeemed through dynamic promotions specific to the customer.
+- Customize the cart completion flow to validate applied loyalty points.
-
+
-- [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).
-
-
-# Send Abandoned Cart Notifications in Medusa
-
-In this tutorial, you will learn how to send notifications to customers who have abandoned their carts.
-
-When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md), which are available out-of-the-box. These features include cart-management capabilities.
-
-Medusa's [Notification Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/index.html.md) allows you to send notifications to users or customers, such as password reset emails, order confirmation SMS, or other types of notifications.
-
-In this tutorial, you will use the Notification Module to send an email to customers who have abandoned their carts. The email will contain a link to recover the customer's cart, encouraging them to complete their purchase. You will use SendGrid to send the emails, but you can also use other email providers.
-
-## Summary
-
-By following this tutorial, you will:
-
-- Install and set up Medusa.
-- Create the logic to send an email to customers who have abandoned their carts.
-- Run the above logic once a day.
-- Add a route to the storefront to recover the cart.
-
-
-
-[View on Github](https://github.com/medusajs/examples/tree/main/abandoned-cart): Find the full code for this tutorial.
-
-***
-
-## Step 1: Install a Medusa Application
-
-### Prerequisites
-
-- [Node.js v20+](https://nodejs.org/en/download)
-- [Git CLI tool](https://git-scm.com/downloads)
-- [PostgreSQL](https://www.postgresql.org/download/)
-
-Start by installing the Medusa application on your machine with the following command:
-
-```bash
-npx create-medusa-app@latest
-```
-
-You will first be asked for the project's name. Then, when asked whether you want to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose "Yes."
-
-Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name and the Next.js Starter Storefront in a separate directory with the `{project-name}-storefront` name.
-
-The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).
-
-Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterwards, you can log in with the new user and explore the dashboard.
-
-Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.
-
-***
-
-## Step 2: Set up SendGrid
-
-### Prerequisites
-
-- [SendGrid account](https://sendgrid.com)
-- [Verified Sender Identity](https://mc.sendgrid.com/senders)
-- [SendGrid API Key](https://app.sendgrid.com/settings/api_keys)
-
-Medusa's Notification Module provides the general functionality to send notifications, but the sending logic is implemented in a module provider. This allows you to integrate the email provider of your choice.
-
-To send the cart-abandonment emails, you will use SendGrid. Medusa provides a [SendGrid Notification Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/sendgrid/index.html.md) that you can use to send emails.
-
-Alternatively, you can use [other Notification Module Providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification#what-is-a-notification-module-provider/index.html.md) or [create a custom provider](https://docs.medusajs.com/references/notification-provider-module/index.html.md).
-
-To set up SendGrid, add the SendGrid Notification Module Provider to `medusa-config.ts`:
-
-```ts title="medusa-config.ts"
-module.exports = defineConfig({
- // ...
- modules: [
- {
- resolve: "@medusajs/medusa/notification",
- options: {
- providers: [
- {
- resolve: "@medusajs/medusa/notification-sendgrid",
- id: "sendgrid",
- options: {
- channels: ["email"],
- api_key: process.env.SENDGRID_API_KEY,
- from: process.env.SENDGRID_FROM,
- },
- },
- ],
- },
- },
- ],
-})
-```
-
-In the `modules` configuration, you pass the Notification Provider and add SendGrid as a provider. You also pass to the SendGrid Module Provider the following options:
-
-- `channels`: The channels that the provider supports. In this case, it is only email.
-- `api_key`: Your SendGrid API key.
-- `from`: The email address that the emails will be sent from.
-
-Then, set the SendGrid API key and "from" email as environment variables, such as in the `.env` file at the root of your project:
-
-```plain
-SENDGRID_API_KEY=your-sendgrid-api-key
-SENDGRID_FROM=test@gmail.com
-```
-
-You can now use SendGrid to send emails in Medusa.
-
-***
-
-## Step 3: Send Abandoned Cart Notification Flow
-
-You will now implement the sending logic for the abandoned cart notifications.
-
-To build custom commerce features in Medusa, you create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow like you construct a function, but it is a special function that allows you to track its executions' progress, define roll-back logic, and configure other advanced features. Then, you execute the workflow from other customizations, such as in a scheduled job.
-
-In this step, you will create the workflow that sends the abandoned cart notifications. Later, you will learn how to execute it once a day.
-
-The workflow will receive the list of abandoned carts as an input. The workflow has the following steps:
-
-- [sendAbandonedNotificationsStep](#sendAbandonedNotificationsStep): Send the abandoned cart notifications.
-- [updateCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartsStep/index.html.md): Update the cart to store the last notification date.
-
-Medusa provides the second step in its `@medusajs/medusa/core-flows` package. So, you only need to implement the first one.
-
-### sendAbandonedNotificationsStep
-
-The first step of the workflow sends a notification to the owners of the abandoned carts that are passed as an input.
-
-To implement the step, create the file `src/workflows/steps/send-abandoned-notifications.ts` with the following content:
-
-```ts title="src/workflows/steps/send-abandoned-notifications.ts"
-import {
- createStep,
- StepResponse,
-} from "@medusajs/framework/workflows-sdk"
-import { Modules } from "@medusajs/framework/utils"
-import { CartDTO, CustomerDTO } from "@medusajs/framework/types"
-
-type SendAbandonedNotificationsStepInput = {
- carts: (CartDTO & {
- customer: CustomerDTO
- })[]
-}
-
-export const sendAbandonedNotificationsStep = createStep(
- "send-abandoned-notifications",
- async (input: SendAbandonedNotificationsStepInput, { container }) => {
- const notificationModuleService = container.resolve(
- Modules.NOTIFICATION
- )
-
- const notificationData = input.carts.map((cart) => ({
- to: cart.email!,
- channel: "email",
- template: process.env.ABANDONED_CART_TEMPLATE_ID || "",
- data: {
- customer: {
- first_name: cart.customer?.first_name || cart.shipping_address?.first_name,
- last_name: cart.customer?.last_name || cart.shipping_address?.last_name,
- },
- cart_id: cart.id,
- items: cart.items?.map((item) => ({
- product_title: item.title,
- quantity: item.quantity,
- unit_price: item.unit_price,
- thumbnail: item.thumbnail,
- })),
- },
- }))
-
- const notifications = await notificationModuleService.createNotifications(
- notificationData
- )
-
- return new StepResponse({
- notifications,
- })
- }
-)
-```
-
-You create a step with `createStep` from the Workflows SDK. It accepts two parameters:
-
-1. The step's unique name, which is `create-review`.
-2. An async function that receives two parameters:
- - The step's input, which is in this case an object with the review's properties.
- - An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step.
-
-In the step function, you first resolve the Notification Module's service, which has methods to manage notifications. Then, you prepare the data of each notification, and create the notifications with the `createNotifications` method.
-
-Notice that each notification is an object with the following properties:
-
-- `to`: The email address of the customer.
-- `channel`: The channel that the notification will be sent through. The Notification Module uses the provider registered for the channel.
-- `template`: The ID or name of the email template in the third-party provider. Make sure to set it as an environment variable once you have it.
-- `data`: The data to pass to the template to render the email's dynamic content.
-
-Based on the dynamic template you create in SendGrid or another provider, you can pass different data in the `data` object.
-
-A step function must return a `StepResponse` instance. The `StepResponse` constructor accepts the step's output as a parameter, which is the created notifications.
-
-### Create Workflow
-
-You can now create the workflow that uses the step you just created to send the abandoned cart notifications.
-
-Create the file `src/workflows/send-abandoned-carts.ts` with the following content:
-
-```ts title="src/workflows/send-abandoned-carts.ts"
-import {
- createWorkflow,
- WorkflowResponse,
- transform,
-} from "@medusajs/framework/workflows-sdk"
-import {
- sendAbandonedNotificationsStep,
-} from "./steps/send-abandoned-notifications"
-import { updateCartsStep } from "@medusajs/medusa/core-flows"
-import { CartDTO } from "@medusajs/framework/types"
-import { CustomerDTO } from "@medusajs/framework/types"
-
-export type SendAbandonedCartsWorkflowInput = {
- carts: (CartDTO & {
- customer: CustomerDTO
- })[]
-}
-
-export const sendAbandonedCartsWorkflow = createWorkflow(
- "send-abandoned-carts",
- function (input: SendAbandonedCartsWorkflowInput) {
- sendAbandonedNotificationsStep(input)
-
- const updateCartsData = transform(
- input,
- (data) => {
- return data.carts.map((cart) => ({
- id: cart.id,
- metadata: {
- ...cart.metadata,
- abandoned_notification: new Date().toISOString(),
- },
- }))
- }
- )
-
- const updatedCarts = updateCartsStep(updateCartsData)
-
- return new WorkflowResponse(updatedCarts)
- }
-)
-```
-
-You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter.
-
-It accepts as a second parameter a constructor function, which is the workflow's implementation. The function can accept input, which in this case is an arra of carts.
-
-In the workflow's constructor function, you:
-
-- Use the `sendAbandonedNotificationsStep` to send the notifications to the carts' customers.
-- Use the `updateCartsStep` from Medusa's core flows to update the carts' metadata with the last notification date.
-
-Notice that you use the `transform` function to prepare the `updateCartsStep`'s input. Medusa does not support direct data manipulation in a workflow's constructor function. You can learn more about it in the [Data Manipulation in Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md).
-
-Your workflow is now ready for use. You will learn how to execute it in the next section.
-
-### Setup Email Template
-
-Before you can test the workflow, you need to set up an email template in SendGrid. The template should contain the dynamic content that you pass in the workflow's step.
-
-To create an email template in SendGrid:
-
-- Go to [Dynamic Templates](https://mc.sendgrid.com/dynamic-templates) in the SendGrid dashboard.
-- Click on the "Create Dynamic Template" button.
-
-
-
-- In the side window that opens, enter a name for the template, then click on the Create button.
-- The template will be added to the middle of the page. When you click on it, a new section will show with an "Add Version" button. Click on it.
-
-
-
-In the form that opens, you can either choose to start with a blank template or from an existing design. You can then use the drag-and-drop or code editor to design the email template.
-
-You can also use the following template as an example:
-
-```html title="Abandoned Cart Email Template"
-
-
-
-
-
- Complete Your Purchase
-
-
-
-
-
Hi {{customer.first_name}}, your cart is waiting! 🛍️
-
You left some great items in your cart. Complete your purchase before they're gone!
-
-
-```
-
-This template will show each item's image, title, quantity, and price in the cart. It will also show a button to return to the cart and checkout.
-
-You can replace `https://yourstore.com` with your storefront's URL. You'll later implement the `/cart/recover/:cart_id` route in the storefront to recover the cart.
-
-Once you are done, copy the template ID from SendGrid and set it as an environment variable in your Medusa project:
-
-```plain
-ABANDONED_CART_TEMPLATE_ID=your-sendgrid-template-id
-```
-
-***
-
-## Step 4: Schedule Cart Abandonment Notifications
-
-The next step is to automate sending the abandoned cart notifications. You need a task that runs once a day to find the carts that have been abandoned for a certain period and send the notifications to the customers.
-
-To run a task at a scheduled interval, you can use a [scheduled job](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md). A scheduled job is an asynchronous function that the Medusa application runs at the interval you specify during the Medusa application's runtime.
-
-You can create a scheduled job in a TypeScript or JavaScript file under the `src/jobs` directory. So, to create the scheduled job that sends the abandoned cart notifications, create the file `src/jobs/send-abandoned-cart-notification.ts` with the following content:
-
-```ts title="src/jobs/send-abandoned-cart-notification.ts"
-import { MedusaContainer } from "@medusajs/framework/types"
-import {
- sendAbandonedCartsWorkflow,
- SendAbandonedCartsWorkflowInput,
-} from "../workflows/send-abandoned-carts"
-
-export default async function abandonedCartJob(
- container: MedusaContainer
-) {
- const logger = container.resolve("logger")
- const query = container.resolve("query")
-
- const oneDayAgo = new Date()
- oneDayAgo.setDate(oneDayAgo.getDate() - 1)
- const limit = 100
- const offset = 0
- const totalCount = 0
- const abandonedCartsCount = 0
-
- do {
- // TODO retrieve paginated abandoned carts
- } while (offset < totalCount)
-
- logger.info(`Sent ${abandonedCartsCount} abandoned cart notifications`)
-}
-
-export const config = {
- name: "abandoned-cart-notification",
- schedule: "0 0 * * *", // Run at midnight every day
-}
-```
-
-In a scheduled job's file, you must export:
-
-1. An asynchronous function that holds the job's logic. The function receives the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) as a parameter.
-2. A `config` object that specifies the job's name and schedule. The schedule is a [cron expression](https://crontab.guru/) that defines the interval at which the job runs.
-
-In the scheduled job function, so far you resolve the [Logger](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md) to log messages, and [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to retrieve data across modules.
-
-You also define a `oneDayAgo` date, which is the date that you will use as the condition of an abandoned cart. In addition, you define variables to paginate the carts.
-
-Next, you will retrieve the abandoned carts using Query. Replace the `TODO` with the following:
-
-```ts title="src/jobs/send-abandoned-cart-notification.ts"
-const {
- data: abandonedCarts,
- metadata,
-} = await query.graph({
- entity: "cart",
- fields: [
- "id",
- "email",
- "items.*",
- "metadata",
- "customer.*",
- ],
- filters: {
- updated_at: {
- $lt: oneDayAgo,
- },
- // @ts-ignore
- email: {
- $ne: null,
- },
- // @ts-ignore
- completed_at: null,
- },
- pagination: {
- skip: offset,
- take: limit,
- },
-})
-
-totalCount = metadata?.count ?? 0
-const cartsWithItems = abandonedCarts.filter((cart) =>
- cart.items?.length > 0 && !cart.metadata?.abandoned_notification
-)
-
-try {
- await sendAbandonedCartsWorkflow(container).run({
- input: {
- carts: cartsWithItems,
- } as unknown as SendAbandonedCartsWorkflowInput,
- })
- abandonedCartsCount += cartsWithItems.length
-
-} catch (error) {
- logger.error(
- `Failed to send abandoned cart notification: ${error.message}`
- )
-}
-
-offset += limit
-```
-
-In the do-while loop, you use Query to retrieve carts matching the following criteria:
-
-- The cart was last updated more than a day ago.
-- The cart has an email address.
-- The cart has not been completed.
-
-You also filter the retrieved carts to only include carts with items and customers that have not received an abandoned cart notification.
-
-Finally, you execute the `sendAbandonedCartsWorkflow` passing it the abandoned carts as an input. You will execute the workflow for each paginated batch of carts.
-
-### Test it Out
-
-To test out the scheduled job and workflow, it is recommended to change the `oneDayAgo` date to a minute before now for easy testing:
-
-```ts title="src/jobs/send-abandoned-cart-notification.ts"
-oneDayAgo.setMinutes(oneDayAgo.getMinutes() - 1) // For testing
-```
-
-And to change the job's schedule in `config` to run every minute:
-
-```ts title="src/jobs/send-abandoned-cart-notification.ts"
-export const config = {
- // ...
- schedule: "* * * * *", // Run every minute for testing
-}
-```
-
-Finally, start the Medusa application with the following command:
-
-```bash npm2yarn
-npm run dev
-```
-
-And in the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md)'s directory (that you installed in the first step), start the storefront with the following command:
-
-```bash npm2yarn
-npm run dev
-```
-
-Open the storefront at `localhost:8000`. You can either:
-
-- Create an account and add items to the cart, then leave the cart for a minute.
-- Add an item to the cart as a guest. Then, start the checkout process, but only enter the shipping and email addresses, and leave the cart for a minute.
-
-Afterwards, wait for the job to execute. Once it is executed, you will see the following message in the terminal:
-
-```bash
-info: Sent 1 abandoned cart notifications
-```
-
-Once you're done testing, make sure to revert the changes to the `oneDayAgo` date and the job's schedule.
-
-***
-
-## Step 5: Recover Cart in Storefront
-
-In the storefront, you need to add a route that recovers the cart when the customer clicks on the link in the email. The route should receive the cart ID, set the cart ID in the cookie, and redirect the customer to the cart page.
-
-To implement the route, in the Next.js Starter Storefront create the file `src/app/[countryCode]/(main)/cart/recover/[id]/route.tsx` with the following content:
-
-```tsx title="src/app/[countryCode]/(main)/cart/recover/[id]/route.tsx" badgeLabel="Storefront" badgeColor="blue"
-import { NextRequest } from "next/server"
-import { retrieveCart } from "../../../../../../lib/data/cart"
-import { setCartId } from "../../../../../../lib/data/cookies"
-import { notFound, redirect } from "next/navigation"
-type Params = Promise<{
- id: string
-}>
-
-export async function GET(req: NextRequest, { params }: { params: Params }) {
- const { id } = await params
- const cart = await retrieveCart(id)
-
- if (!cart) {
- return notFound()
- }
-
- setCartId(id)
-
- const countryCode = cart.shipping_address?.country_code ||
- cart.region?.countries?.[0]?.iso_2
-
- redirect(
- `/${countryCode ? `${countryCode}/` : ""}cart`
- )
-}
-```
-
-You add a `GET` route handler that receives the cart ID as a path parameter. In the route handler, you:
-
-- Try to retrieve the cart from the Medusa application. The `retrieveCart` function is already available in the Next.js Starter Storefront. If the cart is not found, you return a 404 response.
-- Set the cart ID in a cookie using the `setCartId` function. This is also a function that is already available in the storefront.
-- Redirect the customer to the cart page. You set the country code in the URL based on the cart's shipping address or region.
-
-### Test it Out
-
-To test it out, start the Medusa application:
-
-```bash npm2yarn
-npm run dev
-```
-
-And in the Next.js Starter Storefront's directory, start the storefront:
-
-```bash npm2yarn
-npm run dev
-```
-
-Then, either open the link in an abandoned cart email or navigate to `localhost:8000/cart/recover/:cart_id` in your browser. You will be redirected to the cart page with the recovered cart.
-
-
-
-***
-
-## Next Steps
-
-You have now implemented the logic to send abandoned cart notifications in Medusa. You can implement other customizations with Medusa, such as:
-
-- [Implement Product Reviews](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/how-to-tutorials/tutorials/product-reviews/index.html.md).
-- [Implement Wishlist](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/plugins/guides/wishlist/index.html.md).
-- [Allow Custom-Item Pricing](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/examples/guides/custom-item-price/index.html.md).
-
-If you are new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you will get a more in-depth learning of all the concepts you have used in this guide and more.
-
-To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).
-
-
-# Use Saved Payment Methods During Checkout
-
-In this tutorial, you'll learn how to allow customers to save their payment methods and use them for future purchases.
-
-When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md) which are available out-of-the-box.
-
-Medusa's architecture facilitates integrating third-party services, such as payment providers. These payment providers can process payments and securely store customers' payment methods for future use.
-
-In this tutorial, you'll expand on Medusa's [Stripe Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md) to allow customers to re-use their saved payment methods during checkout.
-
-You can follow this guide whether you're new to Medusa or an advanced Medusa developer.
-
-While this tutorial uses Stripe as an example, you can follow the same steps to implement saved payment methods with other payment providers.
-
-## Summary
-
-By following this tutorial, you'll learn how to:
-
-- Install and set up Medusa and the Next.js Starter Storefront.
-- Set up the Stripe Module Provider in Medusa.
-- Customize the checkout flow to save customers' payment methods.
-- Allow customers to select saved payment methods during checkout.
-
-
-
-[Saved Payment Methods Repository](https://github.com/medusajs/examples/tree/main/stripe-saved-payment): Find the full code for this guide in this repository.
+- [Loyalty Points Repository](https://github.com/medusajs/examples/tree/main/loyalty-points): Find the full code for this guide in this repository.
+- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1744212595/OpenApi/Loyalty-Points_jwi5e9.yaml): Import this OpenApi Specs file into tools like Postman.
***
@@ -47634,799 +46845,1746 @@ npx create-medusa-app@latest
You'll first be asked for the project's name. Then, when asked whether you want to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose Yes.
-Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js Starter Storefront in a separate directory with the `{project-name}-storefront` name.
+Afterward, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js Starter Storefront in a separate directory with the `{project-name}-storefront` name.
The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).
-Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterwards, you can log in with the new user and explore the dashboard.
+Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterward, you can log in with the new user and explore the dashboard.
Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.
***
-## Step 2: Set Up the Stripe Module Provider
+## Step 2: Create Loyalty Module
-Medusa's [Payment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/index.html.md) provides payment-related models and the interface to manage and process payments. However, it delegates the actual payment processing to module providers that integrate third-party payment services.
+In Medusa, you can build custom features in a [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). A module is a reusable package with functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup.
-The [Stripe Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md) is a Payment Module Provider that integrates Stripe into your Medusa application to process payments. It can also save payment methods securely.
+In the module, you define the data models necessary for a feature and the logic to manage these data models. Later, you can build commerce flows around your module.
-In this section, you'll set up the Stripe Module Provider in your Medusa application.
+In this step, you'll build a Loyalty Module that defines the necessary data models to store and manage loyalty points for customers.
-### Prerequisites
+Refer to the [Modules documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) to learn more.
-- [Stripe account](https://stripe.com/)
-- [Stripe Secret and Public API Keys](https://support.stripe.com/questions/locate-api-keys-in-the-dashboard)
+### Create Module Directory
-### Register the Stripe Module Provider
+Modules are created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/loyalty`.
-To register the Stripe Module Provider in your Medusa application, add it to the array of providers passed to the Payment Module in `medusa-config.ts`:
+### Create Data Models
+
+A data model represents a table in the database. You create data models using Medusa's Data Model Language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations.
+
+Refer to the [Data Models documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#1-create-data-model/index.html.md) to learn more.
+
+For the Loyalty Module, you need to define a `LoyaltyPoint` data model that represents a customer's loyalty points. So, create the file `src/modules/loyalty/models/loyalty-point.ts` with the following content:
+
+```ts title="src/modules/loyalty/models/loyalty-point.ts" highlights={dmlHighlights}
+import { model } from "@medusajs/framework/utils"
+
+const LoyaltyPoint = model.define("loyalty_point", {
+ id: model.id().primaryKey(),
+ points: model.number().default(0),
+ customer_id: model.text().unique("IDX_LOYALTY_CUSTOMER_ID"),
+})
+
+export default LoyaltyPoint
+```
+
+You define the `LoyaltyPoint` data model using the `model.define` method of the DML. It accepts the data model's table name as a first parameter, and the model's schema object as a second parameter.
+
+The `LoyaltyPoint` data model has the following properties:
+
+- `id`: A unique ID for the loyalty points.
+- `points`: The number of loyalty points a customer has.
+- `customer_id`: The ID of the customer who owns the loyalty points. This property has a unique index to ensure that each customer has only one record in the `loyalty_point` table.
+
+Learn more about defining data model properties in the [Property Types documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties/index.html.md).
+
+### Create Module's Service
+
+You now have the necessary data model in the Loyalty Module, but you'll need to manage its records. You do this by creating a service in the module.
+
+A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to the database, allowing you to manage your data models, or connect to a third-party service, which is useful if you're integrating with external services.
+
+Refer to the [Module Service documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#2-create-service/index.html.md) to learn more.
+
+To create the Loyalty Module's service, create the file `src/modules/loyalty/service.ts` with the following content:
+
+```ts title="src/modules/loyalty/service.ts"
+import { MedusaError, MedusaService } from "@medusajs/framework/utils"
+import LoyaltyPoint from "./models/loyalty-point"
+import { InferTypeOf } from "@medusajs/framework/types"
+
+type LoyaltyPoint = InferTypeOf
+
+class LoyaltyModuleService extends MedusaService({
+ LoyaltyPoint,
+}) {
+ // TODO add methods
+}
+
+export default LoyaltyModuleService
+```
+
+The `LoyaltyModuleService` extends `MedusaService` from the Modules SDK which generates a class with data-management methods for your module's data models. This saves you time on implementing Create, Read, Update, and Delete (CRUD) methods.
+
+So, the `LoyaltyModuleService` class now has methods like `createLoyaltyPoints` and `retrieveLoyaltyPoint`.
+
+Find all methods generated by the `MedusaService` in [the Service Factory reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/index.html.md).
+
+#### Add Methods to the Service
+
+Aside from the basic CRUD methods, you need to add methods that handle custom functionalities related to loyalty points.
+
+First, you need a method that adds loyalty points for a customer. Add the following method to the `LoyaltyModuleService`:
+
+```ts title="src/modules/loyalty/service.ts"
+class LoyaltyModuleService extends MedusaService({
+ LoyaltyPoint,
+}) {
+ async addPoints(customerId: string, points: number): Promise {
+ const existingPoints = await this.listLoyaltyPoints({
+ customer_id: customerId,
+ })
+
+ if (existingPoints.length > 0) {
+ return await this.updateLoyaltyPoints({
+ id: existingPoints[0].id,
+ points: existingPoints[0].points + points,
+ })
+ }
+
+ return await this.createLoyaltyPoints({
+ customer_id: customerId,
+ points,
+ })
+ }
+}
+```
+
+You add an `addPoints` method that accepts two parameters: the ID of the customer and the points to add.
+
+In the method, you retrieve the customer's existing loyalty points using the `listLoyaltyPoints` method, which is automatically generated by the `MedusaService`. If the customer has existing points, you update them with the new points using the `updateLoyaltyPoints` method.
+
+Otherwise, if the customer doesn't have existing loyalty points, you create a new record with the `createLoyaltyPoints` method.
+
+The next method you'll add deducts points from the customer's loyalty points, which is useful when the customer redeems points. Add the following method to the `LoyaltyModuleService`:
+
+```ts title="src/modules/loyalty/service.ts"
+class LoyaltyModuleService extends MedusaService({
+ LoyaltyPoint,
+}) {
+ // ...
+ async deductPoints(customerId: string, points: number): Promise {
+ const existingPoints = await this.listLoyaltyPoints({
+ customer_id: customerId,
+ })
+
+ if (existingPoints.length === 0 || existingPoints[0].points < points) {
+ throw new MedusaError(
+ MedusaError.Types.NOT_ALLOWED,
+ "Insufficient loyalty points"
+ )
+ }
+
+ return await this.updateLoyaltyPoints({
+ id: existingPoints[0].id,
+ points: existingPoints[0].points - points,
+ })
+ }
+}
+```
+
+The `deductPoints` method accepts the customer ID and the points to deduct.
+
+In the method, you retrieve the customer's existing loyalty points using the `listLoyaltyPoints` method. If the customer doesn't have existing points or if the points to deduct are greater than the existing points, you throw an error.
+
+Otherwise, you update the customer's loyalty points with the new value using the `updateLoyaltyPoints` method, which is automatically generated by `MedusaService`.
+
+Next, you'll add the method that retrieves the points of a customer. Add the following method to the `LoyaltyModuleService`:
+
+```ts title="src/modules/loyalty/service.ts"
+class LoyaltyModuleService extends MedusaService({
+ LoyaltyPoint,
+}) {
+ // ...
+ async getPoints(customerId: string): Promise {
+ const points = await this.listLoyaltyPoints({
+ customer_id: customerId,
+ })
+
+ return points[0]?.points || 0
+ }
+}
+```
+
+The `getPoints` method accepts the customer ID and retrieves the customer's loyalty points using the `listLoyaltyPoints` method. If the customer has no points, it returns `0`.
+
+#### Add Method to Map Points to Discount
+
+Finally, you'll add a method that implements the logic of mapping loyalty points to a discount amount. This is useful when the customer wants to redeem their points during checkout.
+
+The mapping logic may differ for each use case. For example, you may need to use a third-party service to map the loyalty points discount amount, or use some custom calculation.
+
+To simplify the logic in this tutorial, you'll use a simple calculation that maps 1 point to 1 currency unit. For example, `100` points = `$100` discount.
+
+Add the following method to the `LoyaltyModuleService`:
+
+```ts title="src/modules/loyalty/service.ts"
+class LoyaltyModuleService extends MedusaService({
+ LoyaltyPoint,
+}) {
+ // ...
+ async calculatePointsFromAmount(amount: number): Promise {
+ // Convert amount to points using a standard conversion rate
+ // For example, $1 = 1 point
+ // Round down to nearest whole point
+ const points = Math.floor(amount)
+
+ if (points < 0) {
+ throw new MedusaError(
+ MedusaError.Types.INVALID_DATA,
+ "Amount cannot be negative"
+ )
+ }
+
+ return points
+ }
+}
+```
+
+The `calculatePointsFromAmount` method accepts the amount and converts it to the nearest whole number of points. If the amount is negative, it throws an error.
+
+You'll use this method later to calculate the amount discounted when a customer redeems their loyalty points.
+
+### Export Module Definition
+
+The final piece to a module is its definition, which you export in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its service.
+
+So, create the file `src/modules/loyalty/index.ts` with the following content:
+
+```ts title="src/modules/loyalty/index.ts"
+import { Module } from "@medusajs/framework/utils"
+import LoyaltyModuleService from "./service"
+
+export const LOYALTY_MODULE = "loyalty"
+
+export default Module(LOYALTY_MODULE, {
+ service: LoyaltyModuleService,
+})
+```
+
+You use the `Module` function from the Modules SDK to create the module's definition. It accepts two parameters:
+
+1. The module's name, which is `loyalty`.
+2. An object with a required property `service` indicating the module's service.
+
+You also export the module's name as `LOYALTY_MODULE` so you can reference it later.
+
+### Add Module to Medusa's Configurations
+
+Once you finish building the module, add it to Medusa's configurations to start using it.
+
+In `medusa-config.ts`, add a `modules` property and pass an array with your custom module:
```ts title="medusa-config.ts"
module.exports = defineConfig({
// ...
modules: [
{
- resolve: "@medusajs/medusa/payment",
- options: {
- providers: [
- {
- resolve: "@medusajs/medusa/payment-stripe",
- id: "stripe",
- options: {
- apiKey: process.env.STRIPE_API_KEY,
- },
- },
- ],
- },
+ resolve: "./src/modules/loyalty",
},
],
})
```
-The Medusa configuration accepts a `modules` array, which contains the modules to be loaded. While the Payment Module is loaded by default, you need to add it again when registering a new provider.
+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 register provides in the `providers` option of the Payment Module. Each provider is an object with the following properties:
+### Generate Migrations
-- `resolve`: The package name of the provider.
-- `id`: The ID of the provider. This is used to identify the provider in the Medusa application.
-- `options`: The options to be passed to the provider. In this case, the `apiKey` option is required for the Stripe Module Provider.
+Since data models represent tables in the database, you define how they're created in the database with migrations. A migration is a TypeScript or JavaScript file that defines database changes made by a module.
-Learn about other options in the [Stripe Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe#module-options/index.html.md) documentation.
+Refer to the [Migrations documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#5-generate-migrations/index.html.md) to learn more.
-### Add Environment Variables
-
-Next, add the following environment variables to your `.env` file:
-
-```plain
-STRIPE_API_KEY=sk_...
-```
-
-Where `STRIPE_API_KEY` is your Stripe Secret API Key. You can find it in the Stripe dashboard under Developers > API keys.
-
-
-
-### Enable Stripe in a Region
-
-In Medusa, each [region](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/region/index.html.md) (which is a geographical area where your store operates) can have different payment methods enabled. So, after registering the Stripe Module Provider, you need to enable it in a region.
-
-To enable it in a region, start the Medusa application with the following command:
-
-```bash npm2yarn
-npm run dev
-```
-
-Then, go to `localhost:9000/app` and log in with the user you created earlier.
-
-Once you're logged in:
-
-1. Go to Settings -> Regions.
-2. Click on the region where you want to enable the payment provider.
-3. Click the icon at the top right of the first section
-4. Choose "Edit" from the dropdown menu
-5. In the side window that opens, find the "Payment Providers" field and select Stripe from the dropdown.
-6. Once you're done, click the "Save" button.
-
-Stripe will now be available as a payment option during checkout.
-
-The Stripe Module Provider supports different payment methods in Stripe, such as Bancontact or iDEAL. This guide focuses only on the card payment method, but you can enable other payment methods as well.
-
-
-
-### Add Evnironement Variable to Storefront
-
-The [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) supports payment with Stripe during checkout if it's enabled in the region.
-
-The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.
-
-So, if your Medusa application's directory is `medusa-payment`, you can find the storefront by going back to the parent directory and changing to the `medusa-payment-storefront` directory:
+Medusa's CLI tool can generate the migrations for you. To generate a migration for the Loyalty Module, run the following command in your Medusa application's directory:
```bash
-cd ../medusa-payment-storefront # change based on your project name
+npx medusa db:generate loyalty
```
-In the Next.js Starter Storefront project, add the Stripe public API key as an environment variable in `.env.local`:
+The `db:generate` command of the Medusa CLI accepts the name of the module to generate the migration for. You'll now have a `migrations` directory under `src/modules/loyalty` that holds the generated migration.
-```plain badgeLabel="Storefront" badgeColor="blue"
-NEXT_PUBLIC_STRIPE_KEY=pk_123...
+Then, to reflect these migrations on the database, run the following command:
+
+```bash
+npx medusa db:migrate
```
-Where `NEXT_PUBLIC_STRIPE_KEY` is your Stripe public API key. You can find it in the Stripe dashboard under Developers > API keys.
+The table for the `LoyaltyPoint` data model is now created in the database.
***
-## Step 3: List Payment Methods API Route
+## Step 3: Change Loyalty Points Flow
-The Payment Module uses [account holders](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/account-holder/index.html.md) to represent a customer's details that are stored in a third-party payment provider. Medusa creates an account holder for each customer, allowing you later to retrieve the customer's saved payment methods in the third-party provider.
+Now that you have a module that stores and manages loyalty points in the database, you'll start building flows around it that allow customers to earn and redeem points.
-
+The first flow you'll build will either add points to a customer's loyalty points or deduct them based on a purchased order. If the customer hasn't redeemed points, the points are added to their loyalty points. Otherwise, the points are deducted from their loyalty points.
-While this feature is available out-of-the-box, you need to expose it to clients, like storefronts, by creating an [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). An API Route is an endpoint that exposes commerce features to external applications and clients.
+To build custom commerce features in Medusa, you create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow like you construct a function, but it's a special function that allows you to track its executions' progress, define roll-back logic, and configure other advanced features. Then, you execute the workflow from other customizations, such as in an endpoint.
-In this step, you'll create an API route that lists the saved payment methods for an authenticated customer.
+In this section, you'll build the workflow that adds or deducts loyalty points for an order's customer. Later, you'll execute this workflow when an order is placed.
-Refer to the [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) documentation to learn more.
+Learn more about workflows in the [Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).
-### Create API Route
+The workflow will have the following steps:
-An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`, and it can include path parameters using square brackets.
+- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the order's details.
+- [validateCustomerExistsStep](#validateCustomerExistsStep): Validate that the customer is registered.
+- [getCartLoyaltyPromoStep](#getCartLoyaltyPromoStep): Retrieve the cart's loyalty promotion.
-So, to create an API route at the path `/store/payment-methods/:account-holder-id`, create the file `src/api/store/payment-methods/[account_holder_id]/route.ts` with the following content:
+Medusa provides the `useQueryGraphStep` and `updatePromotionsStep` in its `@medusajs/medusa/core-flows` package. So, you'll only implement the other steps.
-```ts title="src/api/store/payment-methods/[account_holder_id]/route.ts" highlights={apiRouteHighlights}
+### validateCustomerExistsStep
+
+In the workflow, you first need to validate that the customer is registered. Only registered customers can earn and redeem loyalty points.
+
+To do this, create the file `src/workflows/steps/validate-customer-exists.ts` with the following content:
+
+```ts title="src/workflows/steps/validate-customer-exists.ts"
+import { CustomerDTO } from "@medusajs/framework/types"
+import { createStep } from "@medusajs/framework/workflows-sdk"
import { MedusaError } from "@medusajs/framework/utils"
-import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
-export async function GET(
- req: MedusaRequest,
- res: MedusaResponse
-) {
- const { account_holder_id } = req.params
- const query = req.scope.resolve("query")
- const paymentModuleService = req.scope.resolve("payment")
+export type ValidateCustomerExistsStepInput = {
+ customer: CustomerDTO | null | undefined
+}
- const { data: [accountHolder] } = await query.graph({
- entity: "account_holder",
- fields: [
- "data",
- "provider_id",
- ],
- filters: {
- id: account_holder_id,
- },
- })
+export const validateCustomerExistsStep = createStep(
+ "validate-customer-exists",
+ async ({ customer }: ValidateCustomerExistsStepInput) => {
+ if (!customer) {
+ throw new MedusaError(
+ MedusaError.Types.INVALID_DATA,
+ "Customer not found"
+ )
+ }
- if (!accountHolder) {
- throw new MedusaError(
- MedusaError.Types.NOT_FOUND,
- "Account holder not found"
- )
+ if (!customer.has_account) {
+ throw new MedusaError(
+ MedusaError.Types.INVALID_DATA,
+ "Customer must have an account to earn or manage points"
+ )
+ }
+ }
+)
+```
+
+You create a step with `createStep` from the Workflows SDK. It accepts two parameters:
+
+1. The step's unique name, which is `validate-customer-exists`.
+2. An async function that receives two parameters:
+ - The step's input, which is in this case an object with the customer's details.
+ - An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step.
+
+In the step function, you validate that the customer is defined and that it's registered based on its `has_account` property. Otherwise, you throw an error.
+
+### getCartLoyaltyPromoStep
+
+Next, you'll need to retrieve the loyalty promotion applied on the cart, if there's any. This is useful to determine whether the customer has redeemed points.
+
+Before you create a step, you'll create a utility function that the step uses to retrieve the loyalty promotion of a cart. You'll create it as a separate utility function to use it later in other customizations.
+
+Create the file `src/utils/promo.ts` with the following content:
+
+```ts title="src/utils/promo.ts"
+import { PromotionDTO, CustomerDTO, CartDTO } from "@medusajs/framework/types"
+
+export type CartData = CartDTO & {
+ promotions?: PromotionDTO[]
+ customer?: CustomerDTO
+ metadata: {
+ loyalty_promo_id?: string
+ }
+}
+
+export function getCartLoyaltyPromotion(
+ cart: CartData
+): PromotionDTO | undefined {
+ if (!cart?.metadata?.loyalty_promo_id) {
+ return
}
- const paymentMethods = await paymentModuleService.listPaymentMethods(
- {
- provider_id: accountHolder.provider_id,
- context: {
- account_holder: {
- data: {
- id: accountHolder.data.id,
- },
- },
- },
+ return cart.promotions?.find(
+ (promotion) => promotion.id === cart.metadata.loyalty_promo_id
+ )
+}
+```
+
+You create a `getCartLoyaltyPromotion` function that accepts the cart's details as an input and returns the loyalty promotion if it exists. You retrieve the loyalty promotion if its ID is stored in the cart's `metadata.loyalty_promo_id` property.
+
+You can now create the step that uses this utility to retrieve a carts loyalty points promotion. To create the step, create the file `src/workflows/steps/get-cart-loyalty-promo.ts` with the following content:
+
+```ts title="src/workflows/steps/get-cart-loyalty-promo.ts"
+import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
+import { CartData, getCartLoyaltyPromotion } from "../../utils/promo"
+import { MedusaError } from "@medusajs/framework/utils"
+
+type GetCartLoyaltyPromoStepInput = {
+ cart: CartData,
+ throwErrorOn?: "found" | "not-found"
+}
+
+export const getCartLoyaltyPromoStep = createStep(
+ "get-cart-loyalty-promo",
+ async ({ cart, throwErrorOn }: GetCartLoyaltyPromoStepInput) => {
+ const loyaltyPromo = getCartLoyaltyPromotion(cart)
+
+ if (throwErrorOn === "found" && loyaltyPromo) {
+ throw new MedusaError(
+ MedusaError.Types.INVALID_DATA,
+ "Loyalty promotion already applied to cart"
+ )
+ } else if (throwErrorOn === "not-found" && !loyaltyPromo) {
+ throw new MedusaError(
+ MedusaError.Types.INVALID_DATA,
+ "No loyalty promotion found on cart"
+ )
}
+
+ return new StepResponse(loyaltyPromo)
+ }
+)
+```
+
+You create a step that accepts an object having the following properties:
+
+- `cart`: The cart's details.
+- `throwErrorOn`: An optional property that indicates whether to throw an error if the loyalty promotion is found or not found.
+
+The `throwErrorOn` property is useful to make the step reusable in different scenarios, allowing you to use it in later workflows.
+
+In the step, you call the `getCartLoyaltyPromotion` utility to retrieve the loyalty promotion. If the `throwErrorOn` property is set to `found` and the loyalty promotion is found, you throw an error.
+
+Otherwise, if the `throwErrorOn` property is set to `not-found` and the loyalty promotion is not found, you throw an error.
+
+To return data from a step, you return an instance of `StepResponse` from the Workflows SDK. It accepts as a parameter the data to return, which is the loyalty promotion in this case.
+
+### deductPurchasePointsStep
+
+If the order's cart has a loyalty promotion, you need to deduct points from the customer's loyalty points. To do this, create the file `src/workflows/steps/deduct-purchase-points.ts` with the following content:
+
+```ts title="src/workflows/steps/deduct-purchase-points.ts" highlights={deductStepHighlights} collapsibleLines="1-7" expandButtonLabel="Show Imports"
+import {
+ createStep,
+ StepResponse,
+} from "@medusajs/framework/workflows-sdk"
+import { LOYALTY_MODULE } from "../../modules/loyalty"
+import LoyaltyModuleService from "../../modules/loyalty/service"
+
+type DeductPurchasePointsInput = {
+ customer_id: string
+ amount: number
+}
+
+export const deductPurchasePointsStep = createStep(
+ "deduct-purchase-points",
+ async ({
+ customer_id, amount,
+ }: DeductPurchasePointsInput, { container }) => {
+ const loyaltyModuleService: LoyaltyModuleService = container.resolve(
+ LOYALTY_MODULE
+ )
+
+ const pointsToDeduct = await loyaltyModuleService.calculatePointsFromAmount(
+ amount
+ )
+
+ const result = await loyaltyModuleService.deductPoints(
+ customer_id,
+ pointsToDeduct
+ )
+
+ return new StepResponse(result, {
+ customer_id,
+ points: pointsToDeduct,
+ })
+ },
+ async (data, { container }) => {
+ if (!data) {
+ return
+ }
+
+ const loyaltyModuleService: LoyaltyModuleService = container.resolve(
+ LOYALTY_MODULE
+ )
+
+ // Restore points in case of failure
+ await loyaltyModuleService.addPoints(
+ data.customer_id,
+ data.points
+ )
+ }
+)
+```
+
+You create a step that accepts an object having the following properties:
+
+- `customer_id`: The ID of the customer to deduct points from.
+- `amount`: The promotion's amount, which will be used to calculate the points to deduct.
+
+In the step, you resolve the Loyalty Module's service from the Medusa container. Then, you use the `calculatePointsFromAmount` method to calculate the points to deduct from the promotion's amount.
+
+After that, you call the `deductPoints` method to deduct the points from the customer's loyalty points.
+
+Finally, you return a `StepResponse` with the result of the `deductPoints`.
+
+#### Compensation Function
+
+This step has a compensation function, which is passed as a third parameter to the `createStep` function.
+
+The compensation function undoes the actions performed in a step. Then, if an error occurs during the workflow's execution, the compensation functions of executed steps are called to roll back the changes. This mechanism ensures data consistency in your application, especially as you integrate external systems.
+
+The compensation function accepts two parameters:
+
+1. Data passed from the step function to the compensation function. The data is passed as a second parameter of the returned `StepResponse` instance.
+2. An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md).
+
+In the compensation function, you resolve the Loyalty Module's service from the Medusa container. Then, you call the `addPoints` method to restore the points deducted from the customer's loyalty points if an error occurs.
+
+### addPurchaseAsPointsStep
+
+The last step you'll create adds points to the customer's loyalty points. You'll use this step if the customer didn't redeem points during checkout.
+
+To create the step, create the file `src/workflows/steps/add-purchase-as-points.ts` with the following content:
+
+```ts title="src/workflows/steps/add-purchase-as-points.ts" highlights={addPointsHighlights} collapsibleLines="1-7" expandButtonLabel="Show Imports"
+import {
+ createStep,
+ StepResponse,
+} from "@medusajs/framework/workflows-sdk"
+import { LOYALTY_MODULE } from "../../modules/loyalty"
+import LoyaltyModuleService from "../../modules/loyalty/service"
+
+type StepInput = {
+ customer_id: string
+ amount: number
+}
+
+export const addPurchaseAsPointsStep = createStep(
+ "add-purchase-as-points",
+ async (input: StepInput, { container }) => {
+ const loyaltyModuleService: LoyaltyModuleService = container.resolve(
+ LOYALTY_MODULE
+ )
+
+ const pointsToAdd = await loyaltyModuleService.calculatePointsFromAmount(
+ input.amount
+ )
+
+ const result = await loyaltyModuleService.addPoints(
+ input.customer_id,
+ pointsToAdd
+ )
+
+ return new StepResponse(result, {
+ customer_id: input.customer_id,
+ points: pointsToAdd,
+ })
+ },
+ async (data, { container }) => {
+ if (!data) {
+ return
+ }
+
+ const loyaltyModuleService: LoyaltyModuleService = container.resolve(
+ LOYALTY_MODULE
+ )
+
+ await loyaltyModuleService.deductPoints(
+ data.customer_id,
+ data.points
+ )
+ }
+)
+```
+
+You create a step that accepts an object having the following properties:
+
+- `customer_id`: The ID of the customer to add points to.
+- `amount`: The order's amount, which will be used to calculate the points to add.
+
+In the step, you resolve the Loyalty Module's service from the Medusa container. Then, you use the `calculatePointsFromAmount` method to calculate the points to add from the order's amount.
+
+After that, you call the `addPoints` method to add the points to the customer's loyalty points.
+
+Finally, you return a `StepResponse` with the result of the `addPoints`.
+
+You also pass to the compensation function the customer's ID and the points added. In the compensation function, you deduct the points if an error occurs.
+
+### Add Utility Functions
+
+Before you create the workflow, you need a utility function that checks whether an order's cart has a loyalty promotion. This is useful to determine whether the customer redeemed points during checkout, allowing you to decide which steps to execute.
+
+To add the utility function, add the following to `src/utils/promo.ts`:
+
+```ts title="src/utils/promo.ts"
+import { OrderDTO } from "@medusajs/framework/types"
+
+export type OrderData = OrderDTO & {
+ promotion?: PromotionDTO[]
+ customer?: CustomerDTO
+ cart?: CartData
+}
+
+export const CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE = "customer_id"
+
+export function orderHasLoyaltyPromotion(order: OrderData): boolean {
+ const loyaltyPromotion = getCartLoyaltyPromotion(
+ order.cart as unknown as CartData
+ )
+
+ return loyaltyPromotion?.rules?.some((rule) => {
+ return rule?.attribute === CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE && (
+ rule?.values?.some((value) => value.value === order.customer?.id) || false
+ )
+ }) || false
+}
+```
+
+You first define an `OrderData` type that extends the `OrderDTO` type. This type has the order's details, including the cart, customer, and promotions details.
+
+Then, you define a constant `CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE` that represents the attribute used in the promotion rule to check whether the customer ID is valid.
+
+Finally, you create the `orderHasLoyaltyPromotion` function that accepts an order's details and checks whether it has a loyalty promotion. It returns `true` if:
+
+- The order's cart has a loyalty promotion. You use the `getCartLoyaltyPromotion` utility to try to retrieve the loyalty promotion.
+- The promotion's rules include the `customer_id` attribute and its value matches the order's customer ID.
+ - When you create the promotion for the cart later, you'll see how to set this rule.
+
+You'll use this utility in the workflow next.
+
+### Create the Workflow
+
+Now that you have all the steps, you can create the workflow that uses them.
+
+To create the workflow, create the file `src/workflows/handle-order-points.ts` with the following content:
+
+```ts title="src/workflows/handle-order-points.ts" highlights={handleOrderPointsHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports"
+import { createWorkflow, when } from "@medusajs/framework/workflows-sdk"
+import { updatePromotionsStep, useQueryGraphStep } from "@medusajs/medusa/core-flows"
+import { validateCustomerExistsStep, ValidateCustomerExistsStepInput } from "./steps/validate-customer-exists"
+import { deductPurchasePointsStep } from "./steps/deduct-purchase-points"
+import { addPurchaseAsPointsStep } from "./steps/add-purchase-as-points"
+import { OrderData, CartData } from "../utils/promo"
+import { orderHasLoyaltyPromotion } from "../utils/promo"
+import { getCartLoyaltyPromoStep } from "./steps/get-cart-loyalty-promo"
+
+type WorkflowInput = {
+ order_id: string
+}
+
+export const handleOrderPointsWorkflow = createWorkflow(
+ "handle-order-points",
+ ({ order_id }: WorkflowInput) => {
+ // @ts-ignore
+ const { data: orders } = useQueryGraphStep({
+ entity: "order",
+ fields: [
+ "id",
+ "customer.*",
+ "total",
+ "cart.*",
+ "cart.promotions.*",
+ "cart.promotions.rules.*",
+ "cart.promotions.rules.values.*",
+ "cart.promotions.application_method.*",
+ ],
+ filters: {
+ id: order_id,
+ },
+ options: {
+ throwIfKeyNotFound: true,
+ },
+ })
+
+ validateCustomerExistsStep({
+ customer: orders[0].customer,
+ } as ValidateCustomerExistsStepInput)
+
+ const loyaltyPointsPromotion = getCartLoyaltyPromoStep({
+ cart: orders[0].cart as unknown as CartData,
+ })
+
+ when(orders, (orders) =>
+ orderHasLoyaltyPromotion(orders[0] as unknown as OrderData) &&
+ loyaltyPointsPromotion !== undefined
+ )
+ .then(() => {
+ deductPurchasePointsStep({
+ customer_id: orders[0].customer!.id,
+ amount: loyaltyPointsPromotion.application_method!.value as number,
+ })
+
+ updatePromotionsStep([
+ {
+ id: loyaltyPointsPromotion.id,
+ status: "inactive",
+ },
+ ])
+ })
+
+
+ when(
+ orders,
+ (order) => !orderHasLoyaltyPromotion(order[0] as unknown as OrderData)
+ )
+ .then(() => {
+ addPurchaseAsPointsStep({
+ customer_id: orders[0].customer!.id,
+ amount: orders[0].total,
+ })
+ })
+ }
+)
+```
+
+You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter.
+
+It accepts as a second parameter a constructor function, which is the workflow's implementation. The function can accept input, which in this case is an object with the order's ID.
+
+In the workflow's constructor function, you:
+
+- Use `useQueryGraphStep` to retrieve the order's details. You pass the order's ID as a filter to retrieve the order.
+ - This step uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), which is a tool that retrieves data across modules.
+- Validate that the customer is registered using the `validateCustomerExistsStep`.
+- Retrieve the cart's loyalty promotion using the `getCartLoyaltyPromoStep`.
+- Use `when` to check whether the order's cart has a loyalty promotion.
+ - Since you can't perform data manipulation in a workflow's constructor function, `when` allows you to perform steps if a condition is satisfied.
+ - You pass as a first parameter the object to perform the condition on, which is the order in this case. In the second parameter, you pass a function that returns a boolean value, indicating whether the condition is satisfied.
+ - To specify the steps to perform if a condition is satisfied, you chain a `then` method to the `when` method. You can perform any step within the `then` method.
+ - In this case, if the order's cart has a loyalty promotion, you call the `deductPurchasePointsStep` to deduct points from the customer's loyalty points. You also call the `updatePromotionsStep` to deactivate the cart's loyalty promotion.
+- You use another `when` to check whether the order's cart doesn't have a loyalty promotion.
+ - If the condition is satisfied, you call the `addPurchaseAsPointsStep` to add points to the customer's loyalty points.
+
+You'll use this workflow next when an order is placed.
+
+To learn more about the constraints on a workflow's constructor function, refer to the [Workflow Constraints](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md) documentation. Refer to the [When-Then](https://docs.medusajs.com/docs/learn/fundamentals/workflows/conditions/index.html.md) documentation to learn more about the `when` method and how to use it in a workflow.
+
+***
+
+## Step 4: Handle Order Placed Event
+
+Now that you have the workflow that handles adding or deducting loyalty points for an order, you need to execute it when an order is placed.
+
+Medusa has an event system that allows you to listen to events emitted by the Medusa server using a [subscriber](https://docs.medusajs.com/docs//learn/fundamentals/events-and-subscribers/index.html.md). A subscriber is an asynchronous function that's executed when its associated event is emitted. In a subscriber, you can execute a workflow that performs actions in result of the event.
+
+In this step, you'll create a subscriber that listens to the `order.placed` event and executes the `handleOrderPointsWorkflow` workflow.
+
+Refer to the [Events and Subscribers](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) documentation to learn more.
+
+Subscribers are created in a TypeScript or JavaScript file under the `src/subscribers` directory. So, to create a subscriber, create the fle `src/subscribers/order-placed.ts` with the following content:
+
+```ts title="src/subscribers/order-placed.ts"
+import type {
+ SubscriberArgs,
+ SubscriberConfig,
+} from "@medusajs/framework"
+import { handleOrderPointsWorkflow } from "../workflows/handle-order-points"
+
+export default async function orderPlacedHandler({
+ event: { data },
+ container,
+}: SubscriberArgs<{ id: string }>) {
+ await handleOrderPointsWorkflow(container).run({
+ input: {
+ order_id: data.id,
+ },
+ })
+}
+
+export const config: SubscriberConfig = {
+ event: "order.placed",
+}
+```
+
+The subscriber file must export:
+
+- An asynchronous subscriber function that's executed whenever the associated event, which is `order.placed` is triggered.
+- A configuration object with an event property whose value is the event the subscriber is listening to. You can also pass an array of event names to listen to multiple events in the same subscriber.
+
+The subscriber function accepts an object with the following properties:
+
+- `event`: An object with the event's data payload. For example, the `order.placed` event has the order's ID in its data payload.
+- `container`: The Medusa container, which you can use to resolve services and tools.
+
+In the subscriber function, you execute the `handleOrderPointsWorkflow` by invoking it, passing it the Medusa container, then using its `run` method, passing it the workflow's input.
+
+Whenever an order is placed now, the subscriber will be executed, which in turn will execute the workflow that handles the loyalty points flow.
+
+### Test it Out
+
+To test out the loyalty points flow, you'll use the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) that you installed in the first step. As mentioned in that step, the storefront will be installed in a separate directory from the Medusa application, and its name is `{project-name}-storefront`, where `{project-name}` is the name of your Medusa application's directory.
+
+So, run the following command in the Medusa application's directory to start the Medusa server:
+
+```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green"
+npm run dev
+```
+
+Then, run the following command in the Next.js Starter Storefront's directory to start the Next.js server:
+
+```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
+npm run dev
+```
+
+The Next.js Starter Storefront will be running on `http://localhost:8000`, and the Medusa server will be running on `http://localhost:9000`.
+
+Open the Next.js Starter Storefront in your browser and create a new account by going to Account at the top right.
+
+Once you're logged in, add an item to the cart and go through the checkout flow.
+
+After you place the order, you'll see the following message in your Medusa application's terminal:
+
+```bash
+info: Processing order.placed which has 1 subscribers
+```
+
+This message indicates that the `order.placed` event was emitted, and that your subscriber was executed.
+
+Since you didn't redeem any points during checkout, loyalty points will be added to your account. You'll implement an API route that allows you to retrieve the loyalty points in the next step.
+
+***
+
+## Step 5: Retrieve Loyalty Points API Route
+
+Next, you want to allow customers to view their loyalty points. You can show them on their profile page, or during checkout.
+
+To expose a feature to clients, you create an [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). An API Route is an endpoint that exposes commerce features to external applications and clients, such as storefronts.
+
+You'll create an API route at the path `/store/customers/me/loyalty-points` that returns the loyalty points of the authenticated customer.
+
+Learn more about API routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md).
+
+An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`.
+
+So, to create an API route at the path `/store/customers/me/loyalty-points`, create the file `src/api/store/customers/me/loyalty-points/route.ts` with the following content:
+
+```ts title="src/api/store/customers/me/loyalty-points/route.ts"
+
+import {
+ AuthenticatedMedusaRequest,
+ MedusaResponse,
+} from "@medusajs/framework/http"
+import { LOYALTY_MODULE } from "../../../../../modules/loyalty"
+import LoyaltyModuleService from "../../../../../modules/loyalty/service"
+
+export async function GET(
+ req: AuthenticatedMedusaRequest,
+ res: MedusaResponse
+) {
+ const loyaltyModuleService: LoyaltyModuleService = req.scope.resolve(
+ LOYALTY_MODULE
+ )
+
+ const points = await loyaltyModuleService.getPoints(
+ req.auth_context.actor_id
)
res.json({
- payment_methods: paymentMethods,
+ points,
})
}
```
-Since you export a route handler function named `GET`, you expose a `GET` API route at `/store/payment-methods/:account-holder-id`. The route handler function accepts two parameters:
+Since you export a `GET` route handler function, you're exposing a `GET` endpoint at `/store/customers/me/loyalty-points`. The route handler function accepts two parameters:
1. A request object with details and context on the request, such as body parameters or authenticated customer details.
2. A response object to manipulate and send the response.
-The request object has a `scope` property, which is an instance of the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). The Medusa container is a registry of Framework and commerce tools that you can access in the API route.
+In the route handler, you resolve the Loyalty Module's service from the Medusa container (which is available at `req.scope`).
-You use the Medusa container to resolve:
+Then, you call the service's `getPoints` method to retrieve the authenticated customer's loyalty points. Note that routes starting with `/store/customers/me` are only accessible by authenticated customers. You can access the authenticated customer ID from the request's context, which is available at `req.auth_context.actor_id`.
-- [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), which is a tool that retrieves data across modules in the Medusa application.
-- The [Payment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/index.html.md)'s service, which provides an interface to manage and process payments with third-party providers.
+Finally, you return the loyalty points in the response.
-You use Query to retrieve the account holder with the ID passed as a path parameter. If the account holder is not found, you throw an error.
-
-Then, you use the [listPaymentMethods](https://docs.medusajs.com/references/payment/listPaymentMethods/index.html.md) method of the Payment Module's service to retrieve the payment providers saved in the third-party provider. The method accepts an object with the following properties:
-
-- `provider_id`: The ID of the provider, such as Stripe's ID. The account holder stores the ID its associated provider.
-- `context`: The context of the request. In this case, you pass the account holder's ID to retrieve the payment methods associated with it in the third-party provider.
-
-Finally, you return the payment methods in the response.
-
-### Protect API Route
-
-Only authenticated customers can access and use saved payment methods. So, you need to protect the API route to ensure that only authenticated customers can access it.
-
-To protect an API route, you can add a [middleware](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md). A middleware is a function executed when a request is sent to an API Route. You can add an authentication middleware that ensures that the request is authenticated before executing the route handler function.
-
-Refer to the [Middlewares](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md) documentation to learn more.
-
-Middlewares are added in the `src/api/middlewares.ts` file. So, create the file with the following content:
-
-```ts title="src/api/middlewares.ts"
-import { authenticate, defineMiddlewares } from "@medusajs/framework/http"
-
-export default defineMiddlewares({
- routes: [
- {
- matcher: "/store/payment-methods/:provider_id/:account_holder_id",
- method: "GET",
- middlewares: [
- authenticate("customer", ["bearer", "session"]),
- ],
- },
- ],
-})
-```
-
-The `src/api/middlewares.ts` file must use the `defineMiddlewares` function and export its result. The `defineMiddlewares` function accepts a `routes` array that accepts objects with the following properties:
-
-- `matcher`: The path of the API route to apply the middleware to.
-- `method`: The HTTP method of the API route to apply the middleware to.
-- `middlewares`: An array of middlewares to apply to the API route.
-
-You apply the `authenticate` middleware to the API route you created earlier. The `authenticate` middleware ensures that only authenticated customers can access the API route.
-
-Refer to the [Protected Routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/protected-routes/index.html.md) documentation to learn more about the `authenticate` middleware.
-
-Your API route can now only be accessed by authenticated customers. You'll test it out as you customize the Next.js Starter Storefront in the next steps.
+You'll test out this route as you customize the Next.js Starter Storefront next.
***
-## Step 4: Save Payment Methods During Checkout
+## Step 6: Show Loyalty Points During Checkout
-In this step, you'll customize the checkout flow in the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) to save payment methods during checkout.
+Now that you have the API route to retrieve the loyalty points, you can show them during checkout.
-The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.
+In this step, you'll customize the Next.js Starter Storefront to show the loyalty points in the checkout page.
-So, if your Medusa application's directory is `medusa-payment`, you can find the storefront by going back to the parent directory and changing to the `medusa-payment-storefront` directory:
+First, you'll add a server action function that retrieves the loyalty points from the route you created earlier. In `src/lib/data/customer.ts`, add the following function:
-```bash
-cd ../medusa-payment-storefront # change based on your project name
-```
-
-During checkout, when the customer chooses a payment method, such as Stripe, the Next.js Starter Storefront creates a [payment session](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-session/index.html.md) in Medusa using the [Initialize Payment Session](https://docs.medusajs.com/api/store#payment-collections_postpaymentcollectionsidpaymentsessions) API route.
-
-Under the hood, Medusa uses the associated payment provider (Stripe) to initiate the payment process with the associated third-party payment provider. The [Initialize Payment Session](https://docs.medusajs.com/api/store#payment-collections_postpaymentcollectionsidpaymentsessions) API route accepts a `data` object parameter in the request body that allows you to pass data relevant to the third-party payment provider.
-
-So, to save the payment method that the customer uses during checkout with Stripe, you must pass the `setup_future_usage` property in the `data` object. The `setup_future_usage` property is a Stripe-specific property that allows you to save the payment method for future use.
-
-In `src/modules/checkout/components/payment/index.tsx` of the Next.js Starter Storefront, there are two uses of the `initiatePaymentSession` function. Update each of them to pass the `data` property:
-
-```ts title="src/modules/checkout/components/payment/index.tsx" badgeLabel="Storefront" badgeColor="blue"
-// update in two places
-await initiatePaymentSession(cart, {
- // ...
- data: {
- setup_future_usage: "off_session",
- },
-})
-```
-
-You customize the `initiatePaymentSession` function to pass the `data` object with the `setup_future_usage` property. You set the value to `off_session` to allow using the payment method outside of the checkout flow, such as for follow up payments. You can use `on_session` instead if you only want the payment method to be used by the customer during checkout.
-
-By making this change, you always save the payment method that the customer uses during checkout. You can alternatively show a checkbox to confirm saving the payment method, and only pass the `data` object if the customer checks it.
-
-### Test it Out
-
-To test it out, start the Medusa application by running the following command in the Medusa application's directory:
-
-```bash npm2yarn
-npm run dev
-```
-
-Then, start the Next.js Starter Storefront by running the following command in the storefront's directory:
-
-```bash npm2yarn
-npm run dev
-```
-
-You can open the storefront in your browser at `localhost:8000`. Then, create a new customer account by clicking on the "Account" link at the top right.
-
-After creating an account and logging in, add a product to the cart and go to the checkout page. Once you get to the payment step, choose Stripe and enter a [test card number](https://docs.stripe.com/testing#cards), such as `4242 4242 4242 4242`.
-
-Then, place the order. Once the order is placed, you can check on the Stripe dashboard that the payment method was saved by:
-
-1. Going to the "Customers" section in the Stripe dashboard.
-2. Clicking on the customer you just placed the order with.
-3. Scrolling down to the "Payment methods" section. You'll find the payment method you just used to place the order.
-
-
-
-In the next step, you'll show the saved payment methods to the customer during checkout and allow them to select one of them.
-
-***
-
-## Step 5: Use Saved Payment Methods During Checkout
-
-In this step, you'll customize the checkout flow in the Next.js Starter Storefront to show the saved payment methods to the customer and allow them to select one of them to place the order.
-
-### Retrieve Saved Payment Methods
-
-To retrieve the saved payment methods, you'll add a server function that retrieves the customer's saved payment methods from the API route you created earlier.
-
-Add the following in `src/lib/data/payment.ts`:
-
-```ts title="src/lib/data/payment.ts" badgeLabel="Storefront" badgeColor="blue" highlights={paymentHighlights}
-export type SavedPaymentMethod = {
- id: string
- provider_id: string
- data: {
- card: {
- brand: string
- last4: string
- exp_month: number
- exp_year: number
- }
- }
-}
-
-export const getSavedPaymentMethods = async (accountHolderId: string) => {
+```ts title="src/lib/data/customer.ts" badgeLabel="Storefront" badgeColor="blue"
+export const getLoyaltyPoints = async () => {
const headers = {
...(await getAuthHeaders()),
}
- return sdk.client.fetch<{
- payment_methods: SavedPaymentMethod[]
- }>(
- `/store/payment-methods/${accountHolderId}`,
+ return sdk.client.fetch<{ points: number }>(
+ `/store/customers/me/loyalty-points`,
{
method: "GET",
headers,
}
- ).catch(() => {
- return {
- payment_methods: [],
- }
- })
-}
-```
-
-You define a type for the retrieved payment methods. It contains the following properties:
-
-- `id`: The ID of the payment method in the third-party provider.
-- `provider_id`: The ID of the provider in the Medusa application, such as Stripe's ID.
-- `data`: Additional data retrieved from the third-party provider related to the saved payment method. The type is modeled after the data returned by Stripe, but you can change it to match other payment providers.
-
-You also create a `getSavedPaymentMethods` function that retrieves the saved payment methods from the API route you created earlier. The function accepts the account holder ID as a parameter and returns the saved payment methods.
-
-### Add Saved Payment Methods Component
-
-Next, you'll add the component that shows the saved payment methods and allows the customer to select one of them.
-
-The component that shows the Stripe card element is defined in `src/modules/checkout/components/payment-container/index.tsx`. So, you'll define the component for the saved payment methods in the same file.
-
-Start by adding the following import statements at the top of the file:
-
-```ts title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue"
-import { Button } from "@medusajs/ui"
-import { useEffect, useState } from "react"
-import { HttpTypes } from "@medusajs/types"
-import { SavedPaymentMethod, getSavedPaymentMethods } from "@lib/data/payment"
-import { initiatePaymentSession } from "../../../../lib/data/cart"
-import { capitalize } from "lodash"
-```
-
-Then, update the `PaymentContainerProps` type to include the payment session and cart details:
-
-```ts title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue"
-type PaymentContainerProps = {
- // ...
- paymentSession?: HttpTypes.StorePaymentSession
- cart: HttpTypes.StoreCart
-}
-```
-
-You'll need these details to find which saved payment method the customer selected, and to initiate a new payment session for the cart when the customer chooses a saved payment method.
-
-Next, add the following component at the end of the file:
-
-```tsx title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={stripeSavedPaymentMethodsHighlights}
-const StripeSavedPaymentMethodsContainer = ({
- paymentSession,
- setCardComplete,
- setCardBrand,
- setError,
- cart,
-}: {
- paymentSession?: HttpTypes.StorePaymentSession
- setCardComplete: (complete: boolean) => void
- setCardBrand: (brand: string) => void
- setError: (error: string | null) => void
- cart: HttpTypes.StoreCart
-}) => {
- const [savedPaymentMethods, setSavedPaymentMethods] = useState<
- SavedPaymentMethod[]
- >([])
- const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<
- string | undefined
- >(
- paymentSession?.data?.payment_method_id as string | undefined
)
+ .then(({ points }) => points)
+ .catch(() => null)
+}
+```
+
+You add a `getLoyaltyPoints` function that retrieves the authenticated customer's loyalty points from the API route you created earlier. You pass the authentication headers using the `getAuthHeaders` function, which is a utility function defined in the Next.js Starter Storefront.
+
+If the customer isn't authenticated, the request will fail. So, you catch the error and return `null` in that case.
+
+Next, you'll create a component that shows the loyalty points in the checkout page. Create the file `src/modules/checkout/components/loyalty-points/index.tsx` with the following content:
+
+```tsx title="src/modules/checkout/components/loyalty-points/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={loyaltyPointsHighlights}
+"use client"
+
+import { HttpTypes } from "@medusajs/types"
+import { useEffect, useMemo, useState } from "react"
+import { getLoyaltyPoints } from "../../../../lib/data/customer"
+import { Button, Heading } from "@medusajs/ui"
+import Link from "next/link"
+
+type LoyaltyPointsProps = {
+ cart: HttpTypes.StoreCart & {
+ promotions: HttpTypes.StorePromotion[]
+ }
+}
+
+const LoyaltyPoints = ({ cart }: LoyaltyPointsProps) => {
+ const isLoyaltyPointsPromoApplied = useMemo(() => {
+ return cart.promotions.find(
+ (promo) => promo.id === cart.metadata?.loyalty_promo_id
+ ) !== undefined
+ }, [cart])
+ const [loyaltyPoints, setLoyaltyPoints] = useState<
+ number | null
+ >(null)
useEffect(() => {
- const accountHolderId = (
- paymentSession?.context?.account_holder as Record
- )
- ?.id
-
- if (!accountHolderId) {
- return
- }
-
- getSavedPaymentMethods(accountHolderId)
- .then(({ payment_methods }) => {
- setSavedPaymentMethods(payment_methods)
- })
- }, [paymentSession])
-
- useEffect(() => {
- if (!selectedPaymentMethod || !savedPaymentMethods.length) {
- setCardComplete(false)
- setCardBrand("")
- setError(null)
- return
- }
- const selectedMethod = savedPaymentMethods.find(
- (method) => method.id === selectedPaymentMethod
- )
-
- if (!selectedMethod) {
- return
- }
-
- setCardBrand(capitalize(selectedMethod.data.card.brand))
- setCardComplete(true)
- setError(null)
- }, [selectedPaymentMethod, savedPaymentMethods])
-
- const handleSelect = async (method: SavedPaymentMethod) => {
- // initiate a new payment session with the selected payment method
- await initiatePaymentSession(cart, {
- provider_id: method.provider_id,
- data: {
- payment_method_id: method.id,
- },
- }).catch((error) => {
- setError(error.message)
+ getLoyaltyPoints()
+ .then((points) => {
+ console.log(points)
+ setLoyaltyPoints(points)
})
+ }, [])
- setSelectedPaymentMethod(method.id)
+ const handleTogglePromotion = async (
+ e: React.MouseEvent
+ ) => {
+ e.preventDefault()
+ // TODO apply or remove loyalty promotion
}
- if (!savedPaymentMethods.length) {
- return <>>
- }
-
- // TODO add return statement
-}
-```
-
-You define a `StripeSavedPaymentMethodsContainer` component that accepts the following props:
-
-- `paymentSession`: The cart's current payment session.
-- `setCardComplete`: A function to tell parent components whether the cart or payment method selection is complete. This allows the customer to proceed to the next step in the checkout flow.
-- `setCardBrand`: A function to set the brand of the selected payment method. This is useful to show the brand of the selected payment method in review sections of the checkout flow.
-- `setError`: A function to set the error message in case of an error.
-- `cart`: The cart's details.
-
-In the component, you define a state variable to store the saved payment methods and another one to store the selected payment method.
-
-Then, you use the `useEffect` hook to retrieve the saved payment methods for the account holder set in the cart's payment session. You use the `getSavedPaymentMethods` function you created earlier to retrieve the saved payment methods.
-
-You also use another `useEffect` hook that is executed when the selected payment method changes. In this hook, you check if the selected payment method is valid and set the card brand and completion status accordingly.
-
-Finally, you define a `handleSelect` function that you'll execute when the customer selects a saved payment method. It creates a new payment session with the selected payment method.
-
-To show the saved payment methods, replace the `TODO` with the following `return` statement:
-
-```tsx title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue"
-return (
-
+
+ Loyalty Points
+
+ {loyaltyPoints === null && (
+
+ Sign up to get and use loyalty points
+
+ )}
+ {loyaltyPoints !== null && (
+
+
+
+ You have {loyaltyPoints} loyalty points
+
+ )}
- ))}
-
-)
-```
-
-You display the saved payment methods as radio buttons. When the customer selects one of them, you execute the `handleSelect` function to initiate a new payment session with the selected payment method.
-
-### Modify Existing Stripe Element
-
-Now that you have the component to show the saved payment methods, you need to modify the existing Stripe element to allow customers to select an existing payment method or enter a new one.
-
-In the same `src/modules/checkout/components/payment-container/index.tsx` file, expand the new `paymentSession` and `cart` props of the `StripeCardContainer` component:
-
-```ts title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue"
-export const StripeCardContainer = ({
- // ...
- paymentSession,
- cart,
-}: Omit & {
- // ...
-}) => {
- // ...
+ >
+ )
}
+
+export default LoyaltyPoints
```
-Then, add a new state variable that keeps track of whether the customer is using a saved payment method or entering a new one:
+You create a `LoyaltyPoints` component that accepts the cart's details as a prop. In the component, you:
-```ts title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue"
-const [isUsingSavedPaymentMethod, setIsUsingSavedPaymentMethod] = useState(
- paymentSession?.data?.payment_method_id !== undefined
-)
+- Create a `isLoyaltyPointsPromoApplied` memoized value that checks whether the cart has a loyalty promotion applied. You use the `cart.metadata.loyalty_promo_id` property to check this.
+- Create a `loyaltyPoints` state to store the customer's loyalty points.
+- Call the `getLoyaltyPoints` function in a `useEffect` hook to retrieve the loyalty points from the API route you created earlier. You set the `loyaltyPoints` state with the retrieved points.
+- Define `handleTogglePromotion` that, when clicked, would either apply or remove the promotion. You'll implement these functionalities later.
+- Render the loyalty points in the component. If the customer isn't authenticated, you show a link to the account page to sign up. Otherwise, you show the loyalty points and a button to apply or remove the promotion.
+
+Next, you'll show this component at the end of the checkout's summary component. So, import the component in `src/modules/checkout/templates/checkout-summary/index.tsx`:
+
+```tsx title="src/modules/checkout/templates/checkout-summary/index.tsx" badgeLabel="Storefront" badgeColor="blue"
+import LoyaltyPoints from "../../components/loyalty-points"
```
-Next, add a function that resets the payment session when the customer switches between saved and new payment methods:
+Then, in the return statement of the `CheckoutSummary` component, add the following after the `div` wrapping the `DiscountCode`:
-```ts title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue"
-const handleRefreshSession = async () => {
- await initiatePaymentSession(cart, {
- provider_id: paymentProviderId,
- })
- setIsUsingSavedPaymentMethod(false)
-}
+```tsx title="src/modules/checkout/templates/checkout-summary/index.tsx" badgeLabel="Storefront" badgeColor="blue"
+
```
-This function initiates a new payment session for the cart and disables the `isUsingSavedPaymentMethod` state variable.
-
-Finally, replace the `return` statement of the `StripeCardContainer` component with the following:
-
-```tsx title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={stripeCardReturnHighlights}
-return (
-
- {selectedPaymentOptionId === paymentProviderId &&
- (stripeReady ? (
-
- ) : (
-
- ))}
-
-)
-```
-
-You update the `return` statement to:
-
-- Pass the new `paymentSession` and `cart` props to the `PaymentContainer` component.
-- Show the `StripeSavedPaymentMethodsContainer` component before Stripe's card element.
-- Add a button that's shown when the customer selects a saved payment method. The button allows the customer to switch back to entering a new payment method.
-
-The existing Stripe element in checkout will now show the saved payment methods to the customer along with the component to enter a card's details.
-
-Since you added new props to the `StripeCardContainer` and `PaymentContainer` components, you need to update other components that use them to pass the props.
-
-In `src/modules/checkout/components/payment/index.tsx`, find usages of `StripeCardContainer` and `PaymentContainer` in the return statement and add the `paymentSession` and `cart` props:
-
-```tsx title="src/modules/checkout/components/payment/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["5"], ["6"], ["11"], ["12"]]}
-
-```
-
-### Support Updating Stripe's Client Secret
-
-The Next.js Starter Storefront uses Stripe's `Elements` component to wrap the payment elements. The `Elements` component requires a `clientSecret` prop, which is available in the cart's payment session.
-
-With the recent changes, the client secret will be updated whenever a payment session is initiated, such as when the customer selects a saved payment method. However, the `options.clientSecret` prop of the `Elements` component is immutable, meaning that it cannot be changed after the component is mounted.
-
-To force the component to re-mount and update the `clientSecret` prop, you can add a `key` prop to the `Elements` component. The `key` prop ensures that the `Elements` component re-mounts whenever the client secret changes, allowing Stripe to process the updated payment session.
-
-In `src/modules/checkout/components/payment-wrapper/stripe-wrapper.tsx`, find the `Elements` component in the `return` statement and add the `key` prop:
-
-```tsx title="src/modules/checkout/components/payment-wrapper/stripe-wrapper.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["4"]]}
-
- {children}
-
-```
-
-You set the `key` prop to the client secret, which forces the `Elements` component to re-mount whenever the client secret changes.
-
-### Support Payment with Saved Payment Method
-
-The last change you need to make ensures that the customer can place an order with a saved payment method.
-
-When the customer places the order, and they've chosen Stripe as a payment method, the Next.js Starter Storefront uses Stripe's `confirmCardPayment` method to confirm the payment. This method accepts either the ID of a saved payment method, or the details of a new card.
-
-So, you need to update the `confirmCardPayment` usage to support passing the ID of the selected payment method if the customer has selected one.
-
-In `src/modules/checkout/components/payment-button/index.tsx`, find the `handlePayment` method and update its first `if` condition:
-
-```ts title="src/modules/checkout/components/payment-button/index.tsx" badgeLabel="Storefront" badgeColor="blue"
-if (!stripe || !elements || (!card && !session?.data.payment_method_id) || !cart) {
- setSubmitting(false)
- return
-}
-```
-
-This allows the customer to place their order if they have selected a saved payment method but have not entered a new card.
-
-Then, find the usage of `confirmCardPayment` in the `handlePayment` function and change it to the following:
-
-```ts title="src/modules/checkout/components/payment-button/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={confirmPaymentHighlights}
-await stripe
-.confirmCardPayment(session?.data.client_secret as string, {
- payment_method: session?.data.payment_method_id as string || {
- card: card!,
- billing_details: {
- name:
- cart.billing_address?.first_name +
- " " +
- cart.billing_address?.last_name,
- address: {
- city: cart.billing_address?.city ?? undefined,
- country: cart.billing_address?.country_code ?? undefined,
- line1: cart.billing_address?.address_1 ?? undefined,
- line2: cart.billing_address?.address_2 ?? undefined,
- postal_code: cart.billing_address?.postal_code ?? undefined,
- state: cart.billing_address?.province ?? undefined,
- },
- email: cart.email,
- phone: cart.billing_address?.phone ?? undefined,
- },
- },
-})
-.then(({ error, paymentIntent }) => {
- if (error) {
- const pi = error.payment_intent
-
- if (
- (pi && pi.status === "requires_capture") ||
- (pi && pi.status === "succeeded")
- ) {
- onPaymentCompleted()
- }
-
- setErrorMessage(error.message || null)
- return
- }
-
- if (
- (paymentIntent && paymentIntent.status === "requires_capture") ||
- paymentIntent.status === "succeeded"
- ) {
- return onPaymentCompleted()
- }
-
- return
-})
-```
-
-In particular, you're changing the `payment_method` property to either be the ID of the selected payment method, or the details of a new card. This allows the customer to place an order with either a saved payment method or a new one.
+This will show the loyalty points component at the end of the checkout summary.
### Test it Out
-You can now test out placing orders with a saved payment method.
+To test out the customizations to the checkout flow, make sure both the Medusa application and Next.js Starter Storefront are running.
-To do that, start the Medusa application by running the following command in the Medusa application's directory:
+Then, as an authenticated customer, add an item to cart and proceed to checkout. You'll find a new "Loyalty Points" section at the end of the checkout summary.
-```bash npm2yarn
-npm run dev
+
+
+If you made a purchase before, you can see your loyalty points. You'll also see the "Apply Loyalty Points" button, which doesn't yet do anything. You'll add the functionality next.
+
+***
+
+## Step 7: Apply Loyalty Points to Cart
+
+The next feature you'll implement allows the customer to apply their loyalty points during checkout. To implement the feature, you need:
+
+- A workflow that implements the steps of the apply loyalty points flow.
+- An API route that exposes the workflow's functionality to clients. You'll then send a request to this API route to apply the loyalty points on the customer's cart.
+- A function in the Next.js Starter Storefront that sends the request to the API route you created earlier.
+
+The workflow will have the following steps:
+
+- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details.
+- [validateCustomerExistsStep](#validateCustomerExistsStep): Validate that the customer is registered.
+- [getCartLoyaltyPromoStep](#getCartLoyaltyPromoStep): Retrieve the cart's loyalty promotion.
+- [getCartLoyaltyPromoAmountStep](#getCartLoyaltyPromoAmountStep): Get the amount to be discounted based on the loyalty points.
+- [createPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPromotionsStep/index.html.md): Create a new loyalty promotion for the cart.
+- [updateCartPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/workflows/updateCartPromotionsWorkflow/index.html.md): Update the cart's promotions with the new loyalty promotion.
+- [updateCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartsStep/index.html.md): Update the cart to store the ID of the loyalty promotion in the metadata.
+- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details again.
+
+Most of the workflow's steps are either provided by Medusa in the `@medusajs/medusa/core-flows` package or steps you've already implemented. You only need to implement the `getCartLoyaltyPromoAmountStep` step.
+
+### getCartLoyaltyPromoAmountStep
+
+The fourth step in the workflow is the `getCartLoyaltyPromoAmountStep`, which retrieves the amount to be discounted based on the loyalty points. This step is useful to determine how much discount to apply to the cart.
+
+To create the step, create the file `src/workflows/steps/get-cart-loyalty-promo-amount.ts` with the following content:
+
+```ts title="src/workflows/steps/get-cart-loyalty-promo-amount.ts" highlights={getCartLoyaltyPromoAmountStepHighlights}
+import { PromotionDTO, CustomerDTO } from "@medusajs/framework/types"
+import { MedusaError } from "@medusajs/framework/utils"
+import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
+import LoyaltyModuleService from "../../modules/loyalty/service"
+import { LOYALTY_MODULE } from "../../modules/loyalty"
+
+export type GetCartLoyaltyPromoAmountStepInput = {
+ cart: {
+ id: string
+ customer: CustomerDTO
+ promotions?: PromotionDTO[]
+ total: number
+ }
+}
+
+export const getCartLoyaltyPromoAmountStep = createStep(
+ "get-cart-loyalty-promo-amount",
+ async ({ cart }: GetCartLoyaltyPromoAmountStepInput, { container }) => {
+ // Check if customer has any loyalty points
+ const loyaltyModuleService: LoyaltyModuleService = container.resolve(
+ LOYALTY_MODULE
+ )
+ const loyaltyPoints = await loyaltyModuleService.getPoints(
+ cart.customer.id
+ )
+
+ if (loyaltyPoints <= 0) {
+ throw new MedusaError(
+ MedusaError.Types.INVALID_DATA,
+ "Customer has no loyalty points"
+ )
+ }
+
+ const pointsAmount = await loyaltyModuleService.calculatePointsFromAmount(
+ loyaltyPoints
+ )
+
+ const amount = Math.min(pointsAmount, cart.total)
+
+ return new StepResponse(amount)
+ }
+)
```
-Then, start the Next.js Starter Storefront by running the following command in the storefront's directory:
+You create a step that accepts an object having the cart's details.
-```bash npm2yarn
-npm run dev
+In the step, you resolve the Loyalty Module's service from the Medusa container. Then, you call the `getPoints` method to retrieve the customer's loyalty points. If the customer has no loyalty points, you throw an error.
+
+Next, you call the `calculatePointsFromAmount` method to calculate the amount to be discounted based on the loyalty points. You use the `Math.min` function to ensure that the amount doesn't exceed the cart's total.
+
+Finally, you return a `StepResponse` with the amount to be discounted.
+
+### Create the Workflow
+
+You can now create the workflow that applies a loyalty promotion to the cart.
+
+To create the workflow, create the file `src/workflows/apply-loyalty-on-cart.ts` with the following content:
+
+```ts title="src/workflows/apply-loyalty-on-cart.ts" highlights={applyLoyaltyOnCartWorkflowHighlights} collapsibleLines="1-24" expandButtonLabel="Show Imports"
+import {
+ createWorkflow,
+ transform,
+ WorkflowResponse,
+} from "@medusajs/framework/workflows-sdk"
+import {
+ createPromotionsStep,
+ updateCartPromotionsWorkflow,
+ updateCartsStep,
+ useQueryGraphStep,
+} from "@medusajs/medusa/core-flows"
+import {
+ validateCustomerExistsStep,
+ ValidateCustomerExistsStepInput,
+} from "./steps/validate-customer-exists"
+import {
+ getCartLoyaltyPromoAmountStep,
+ GetCartLoyaltyPromoAmountStepInput,
+} from "./steps/get-cart-loyalty-promo-amount"
+import { CartData, CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE } from "../utils/promo"
+import { CreatePromotionDTO } from "@medusajs/framework/types"
+import { PromotionActions } from "@medusajs/framework/utils"
+import { getCartLoyaltyPromoStep } from "./steps/get-cart-loyalty-promo"
+
+type WorkflowInput = {
+ cart_id: string
+}
+
+const fields = [
+ "id",
+ "customer.*",
+ "promotions.*",
+ "promotions.application_method.*",
+ "promotions.rules.*",
+ "promotions.rules.values.*",
+ "currency_code",
+ "total",
+ "metadata",
+]
+
+export const applyLoyaltyOnCartWorkflow = createWorkflow(
+ "apply-loyalty-on-cart",
+ (input: WorkflowInput) => {
+ // @ts-ignore
+ const { data: carts } = useQueryGraphStep({
+ entity: "cart",
+ fields,
+ filters: {
+ id: input.cart_id,
+ },
+ options: {
+ throwIfKeyNotFound: true,
+ },
+ })
+
+ validateCustomerExistsStep({
+ customer: carts[0].customer,
+ } as ValidateCustomerExistsStepInput)
+
+ getCartLoyaltyPromoStep({
+ cart: carts[0] as unknown as CartData,
+ throwErrorOn: "found",
+ })
+
+ const amount = getCartLoyaltyPromoAmountStep({
+ cart: carts[0],
+ } as unknown as GetCartLoyaltyPromoAmountStepInput)
+
+ // TODO create and apply the promotion on the cart
+ }
+)
```
-In the Next.js Starter Storefront, login with the customer account you created earlier and add a product to the cart.
+You create a workflow that accepts an object with the cart's ID as input.
-Then, proceed to the checkout flow. In the payment step, you should see the saved payment method you used earlier. You can select it and place the order.
+So far, you:
-
+- Use `useQueryGraphStep` to retrieve the cart's details. You pass the cart's ID as a filter to retrieve the cart.
+- Validate that the customer is registered using the `validateCustomerExistsStep`.
+- Check whether the cart has a loyalty promotion using the `getCartLoyaltyPromoStep`. You pass the `throwErrorOn` parameter with the value `found` to throw an error if a loyalty promotion is found in the cart.
+- Retrieve the amount to be discounted based on the loyalty points using the `getCartLoyaltyPromoAmountStep`.
-Once the order is placed successfully, you can check it in the Medusa Admin dashboard. You can view the order and capture the payment.
+Next, you need to create a new loyalty promotion for the cart. First, you'll prepare the data of the promotion to be created.
-
+Replace the `TODO` with the following:
+
+```ts title="src/workflows/apply-loyalty-on-cart.ts" highlights={prepareLoyaltyPromoDataHighlights}
+const promoToCreate = transform({
+ carts,
+ amount,
+}, (data) => {
+ const randomStr = Math.random().toString(36).substring(2, 8)
+ const uniqueId = (
+ "LOYALTY-" + data.carts[0].customer?.first_name + "-" + randomStr
+ ).toUpperCase()
+ return {
+ code: uniqueId,
+ type: "standard",
+ status: "active",
+ application_method: {
+ type: "fixed",
+ value: data.amount,
+ target_type: "order",
+ currency_code: data.carts[0].currency_code,
+ allocation: "across",
+ },
+ rules: [
+ {
+ attribute: CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE,
+ operator: "eq",
+ values: [data.carts[0].customer!.id],
+ },
+ ],
+ campaign: {
+ name: uniqueId,
+ description: "Loyalty points promotion for " + data.carts[0].customer!.email,
+ campaign_identifier: uniqueId,
+ budget: {
+ type: "usage",
+ limit: 1,
+ },
+ },
+ }
+})
+
+// TODO create promotion and apply it on cart
+```
+
+Since data manipulation isn't allowed in a workflow constructor, you use the [transform](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) function from the Workflows SDK. It accepts two parameters:
+
+- The data to perform manipulation on. In this case, you pass the cart's details and the amount to be discounted.
+- A function that receives the data from the first parameter, and returns the transformed data.
+
+In the transformation function, you prepare th data of the loyalty promotion to be created. Some key details include:
+
+- You set the discount amount in the application method of the promotion.
+- You add a rule to the promotion that ensures it can be used only in carts having their `customer_id` equal to this customer's ID. This prevents other customers from using this promotion.
+- You create a campaign for the promotion, and you set the campaign budget to a single usage. This prevents the customer from using the promotion again.
+
+Learn more about promotion concepts in the [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md)'s documentation.
+
+You can now use the returned data to create a promotion and apply it to the cart. Replace the new `TODO` with the following:
+
+```ts title="src/workflows/apply-loyalty-on-cart.ts" highlights={createLoyaltyPromoStepHighlights}
+const loyaltyPromo = createPromotionsStep([
+ promoToCreate,
+] as CreatePromotionDTO[])
+
+const { metadata, ...updatePromoData } = transform({
+ carts,
+ promoToCreate,
+ loyaltyPromo,
+}, (data) => {
+ const promos = [
+ ...(data.carts[0].promotions?.map((promo) => promo?.code).filter(Boolean) || []) as string[],
+ data.promoToCreate.code,
+ ]
+
+ return {
+ cart_id: data.carts[0].id,
+ promo_codes: promos,
+ action: PromotionActions.ADD,
+ metadata: {
+ loyalty_promo_id: data.loyaltyPromo[0].id,
+ },
+ }
+})
+
+updateCartPromotionsWorkflow.runAsStep({
+ input: updatePromoData,
+})
+
+updateCartsStep([
+ {
+ id: input.cart_id,
+ metadata,
+ },
+])
+
+// retrieve cart with updated promotions
+// @ts-ignore
+const { data: updatedCarts } = useQueryGraphStep({
+ entity: "cart",
+ fields,
+ filters: { id: input.cart_id },
+}).config({ name: "retrieve-cart" })
+
+return new WorkflowResponse(updatedCarts[0])
+```
+
+In the rest of the workflow, you:
+
+- Create the loyalty promotion using the data you prepared earlier using the `createPromotionsStep`.
+- Use the `transform` function to prepare the data to update the cart's promotions. You add the new loyalty promotion code to the cart's promotions codes, and set the `loyalty_promo_id` in the cart's metadata.
+- Update the cart's promotions with the new loyalty promotion using the `updateCartPromotionsWorkflow` workflow.
+- Update the cart's metadata with the loyalty promotion ID using the `updateCartsStep`.
+- Retrieve the cart's details again using `useQueryGraphStep` to get the updated cart with the new loyalty promotion.
+
+To return data from the workflow, you must return an instance of `WorkflowResponse`. You pass it the data to be returned, which is in this case the cart's details.
+
+### Create the API Route
+
+Next, you'll create the API route that executes this workflow.
+
+To create the API route, create the file `src/api/store/carts/[id]/loyalty-points/route.ts` with the following content:
+
+```ts title="src/api/store/carts/[id]/loyalty-points/route.ts"
+import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
+import { applyLoyaltyOnCartWorkflow } from "../../../../../workflows/apply-loyalty-on-cart"
+
+export async function POST(
+ req: MedusaRequest,
+ res: MedusaResponse
+) {
+ const { id: cart_id } = req.params
+
+ const { result: cart } = await applyLoyaltyOnCartWorkflow(req.scope)
+ .run({
+ input: {
+ cart_id,
+ },
+ })
+
+ res.json({ cart })
+}
+```
+
+Since you export a `POST` route handler, you expose a `POST` API route at `/store/carts/[id]/loyalty-points`.
+
+In the route handler, you execute the `applyLoyaltyOnCartWorkflow` workflow, passing it the cart ID as an input. You return the cart's details in the response.
+
+You can now use this API route in the Next.js Starter Storefront.
+
+### Apply Loyalty Points in the Storefront
+
+In the Next.js Starter Storefront, you need to add a server action function that sends a request to the API route you created earlier. Then, you'll use that function when the customer clicks the "Apply Loyalty Points" button.
+
+To add the function, add the following to `src/lib/data/cart.ts` in the Next.js Starter Storefront:
+
+```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="blue"
+export async function applyLoyaltyPointsOnCart() {
+ const cartId = await getCartId()
+ const headers = {
+ ...(await getAuthHeaders()),
+ }
+
+ return await sdk.client.fetch<{
+ cart: HttpTypes.StoreCart & {
+ promotions: HttpTypes.StorePromotion[]
+ }
+ }>(`/store/carts/${cartId}/loyalty-points`, {
+ method: "POST",
+ headers,
+ })
+ .then(async (result) => {
+ const cartCacheTag = await getCacheTag("carts")
+ revalidateTag(cartCacheTag)
+
+ return result
+ })
+}
+```
+
+You create an `applyLoyaltyPointsOnCart` function that sends a request to the API route you created earlier.
+
+In the function, you retrieve the cart ID stored in the cookie using the `getCartId` function, which is available in the Next.js Starter Storefront.
+
+Then, you send the request. Once the request is resolved successfully, you revalidate the cart cache tag to ensure that the cart's details are updated and refetched by other components. This ensures that the applied promotion is shown in the checkout summary without needing to refresh the page.
+
+Finally, you'll use this function in the `handleTogglePromotion` function in the `LoyaltyPoints` component you created earlier.
+
+At the top of `src/modules/checkout/components/loyalty-points/index.tsx`, import the function:
+
+```tsx title="src/modules/checkout/components/loyalty-points/index.tsx" badgeLabel="Storefront" badgeColor="blue"
+import { applyLoyaltyPointsOnCart } from "../../../../lib/data/cart"
+```
+
+Then, replace the `handleTogglePromotion` function with the following:
+
+```tsx title="src/modules/checkout/components/loyalty-points/index.tsx" badgeLabel="Storefront" badgeColor="blue"
+const handleTogglePromotion = async (
+ e: React.MouseEvent
+) => {
+ e.preventDefault()
+ if (!isLoyaltyPointsPromoApplied) {
+ await applyLoyaltyPointsOnCart()
+ } else {
+ // TODO remove loyalty points
+ }
+}
+```
+
+In the `handleTogglePromotion` function, you call the `applyLoyaltyPointsOnCart` function if the cart doesn't have a loyalty promotion. This will send a request to the API route you created earlier, which will execute the workflow that applies the loyalty promotion to the cart.
+
+You'll implement removing the loyalty points promotion in a later step.
+
+### Test it Out
+
+To test out applying the loyalty points on the cart, start the Medusa application and Next.js Starter Storefront.
+
+Then, in the checkout flow as an authenticated customer, click on the "Apply Loyalty Points" button. The checkout summary will be updated with the applied promotion and the discount amount.
+
+If you don't want the promotion to be shown in the "Promotions(s) applied" section, you can filter the promotions in `src/modules/checkout/components/discount-code/index.tsx` to not show a promotion matching `cart.metadata.loyalty_promo_id`.
+
+
+
+***
+
+## Step 8: Remove Loyalty Points From Cart
+
+In this step, you'll implement the functionality to remove the loyalty points promotion from the cart. This is useful if the customer changes their mind and wants to remove the promotion.
+
+To implement this functionality, you'll need to:
+
+- Create a workflow that removes the loyalty points promotion from the cart.
+- Create an API route that executes the workflow.
+- Create a function in the Next.js Starter Storefront that sends a request to the API route you created earlier.
+- Use the function in the `handleTogglePromotion` function in the `LoyaltyPoints` component you created earlier.
+
+### Create the Workflow
+
+The workflow will have the following steps:
+
+- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details.
+- [getCartLoyaltyPromoStep](#getCartLoyaltyPromoStep): Retrieve the cart's loyalty promotion.
+- [updateCartPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/workflows/updateCartPromotionsWorkflow/index.html.md): Update the cart's promotions to remove the loyalty promotion.
+- [updateCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartsStep/index.html.md): Update the cart to remove the loyalty promotion ID from the metadata.
+- [updatePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePromotionsStep/index.html.md): Deactive the loyalty promotion.
+- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details again.
+
+Since you already have all the steps, you can create the workflow.
+
+To create the workflow, create the file `src/workflows/remove-loyalty-from-cart.ts` with the following content:
+
+```ts title="src/workflows/remove-loyalty-from-cart.ts" collapsibleLines="1-15" expandButtonLabel="Show Imports" highlights={removeLoyaltyFromCartWorkflowHighlights}
+import {
+ createWorkflow,
+ transform,
+ WorkflowResponse,
+} from "@medusajs/framework/workflows-sdk"
+import {
+ useQueryGraphStep,
+ updateCartPromotionsWorkflow,
+ updateCartsStep,
+ updatePromotionsStep,
+} from "@medusajs/medusa/core-flows"
+import { getCartLoyaltyPromoStep } from "./steps/get-cart-loyalty-promo"
+import { PromotionActions } from "@medusajs/framework/utils"
+import { CartData } from "../utils/promo"
+
+type WorkflowInput = {
+ cart_id: string
+}
+
+const fields = [
+ "id",
+ "customer.*",
+ "promotions.*",
+ "promotions.application_method.*",
+ "promotions.rules.*",
+ "promotions.rules.values.*",
+ "currency_code",
+ "total",
+ "metadata",
+]
+
+export const removeLoyaltyFromCartWorkflow = createWorkflow(
+ "remove-loyalty-from-cart",
+ (input: WorkflowInput) => {
+ // @ts-ignore
+ const { data: carts } = useQueryGraphStep({
+ entity: "cart",
+ fields,
+ filters: {
+ id: input.cart_id,
+ },
+ })
+
+ const loyaltyPromo = getCartLoyaltyPromoStep({
+ cart: carts[0] as unknown as CartData,
+ throwErrorOn: "not-found",
+ })
+
+ updateCartPromotionsWorkflow.runAsStep({
+ input: {
+ cart_id: input.cart_id,
+ promo_codes: [loyaltyPromo.code!],
+ action: PromotionActions.REMOVE,
+ },
+ })
+
+ const newMetadata = transform({
+ carts,
+ }, (data) => {
+ const { loyalty_promo_id, ...rest } = data.carts[0].metadata || {}
+
+ return {
+ ...rest,
+ loyalty_promo_id: null,
+ }
+ })
+
+ updateCartsStep([
+ {
+ id: input.cart_id,
+ metadata: newMetadata,
+ },
+ ])
+
+ updatePromotionsStep([
+ {
+ id: loyaltyPromo.id,
+ status: "inactive",
+ },
+ ])
+
+ // retrieve cart with updated promotions
+ // @ts-ignore
+ const { data: updatedCarts } = useQueryGraphStep({
+ entity: "cart",
+ fields,
+ filters: { id: input.cart_id },
+ }).config({ name: "retrieve-cart" })
+
+ return new WorkflowResponse(updatedCarts[0])
+ }
+)
+```
+
+You create a workflow that accepts an object with the cart's ID as input.
+
+In the workflow, you:
+
+- Use `useQueryGraphStep` to retrieve the cart's details. You pass the cart's ID as a filter to retrieve the cart.
+- Check whether the cart has a loyalty promotion using the `getCartLoyaltyPromoStep`. You pass the `throwErrorOn` parameter with the value `not-found` to throw an error if a loyalty promotion isn't found in the cart.
+- Update the cart's promotions using the `updateCartPromotionsWorkflow`, removing the loyalty promotion.
+- Use the `transform` function to prepare the new metadata of the cart. You remove the `loyalty_promo_id` from the metadata.
+- Update the cart's metadata with the new metadata using the `updateCartsStep`.
+- Deactivate the loyalty promotion using the `updatePromotionsStep`.
+- Retrieve the cart's details again using `useQueryGraphStep` to get the updated cart with the new loyalty promotion.
+- Return the cart's details in a `WorkflowResponse` instance.
+
+### Create the API Route
+
+Next, you'll create the API route that executes this workflow.
+
+To create the API route, add the following in `src/api/store/carts/[id]/loyalty-points/route.ts`:
+
+```ts title="src/api/store/carts/[id]/loyalty-points/route.ts"
+// other imports...
+import { removeLoyaltyFromCartWorkflow } from "../../../../../workflows/remove-loyalty-from-cart"
+
+// ...
+export async function DELETE(
+ req: MedusaRequest,
+ res: MedusaResponse
+) {
+ const { id: cart_id } = req.params
+
+ const { result: cart } = await removeLoyaltyFromCartWorkflow(req.scope)
+ .run({
+ input: {
+ cart_id,
+ },
+ })
+
+ res.json({ cart })
+}
+```
+
+You export a `DELETE` route handler, which exposes a `DELETE` API route at `/store/carts/[id]/loyalty-points`.
+
+In the route handler, you execute the `removeLoyaltyFromCartWorkflow` workflow, passing it the cart ID as an input. You return the cart's details in the response.
+
+You can now use this API route in the Next.js Starter Storefront.
+
+### Remove Loyalty Points in the Storefront
+
+In the Next.js Starter Storefront, you need to add a server action function that sends a request to the API route you created earlier. Then, you'll use that function when the customer clicks the "Remove Loyalty Points" button, which shows when the cart has a loyalty promotion applied.
+
+To add the function, add the following to `src/lib/data/cart.ts`:
+
+```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="blue"
+export async function removeLoyaltyPointsOnCart() {
+ const cartId = await getCartId()
+ const headers = {
+ ...(await getAuthHeaders()),
+ }
+ const next = {
+ ...(await getCacheOptions("carts")),
+ }
+
+ return await sdk.client.fetch<{
+ cart: HttpTypes.StoreCart & {
+ promotions: HttpTypes.StorePromotion[]
+ }
+ }>(`/store/carts/${cartId}/loyalty-points`, {
+ method: "DELETE",
+ headers,
+ })
+ .then(async (result) => {
+ const cartCacheTag = await getCacheTag("carts")
+ revalidateTag(cartCacheTag)
+
+ return result
+ })
+}
+```
+
+You create a `removeLoyaltyPointsOnCart` function that sends a request to the API route you created earlier.
+
+In the function, you retrieve the cart ID stored in the cookie using the `getCartId` function, which is available in the Next.js Starter Storefront.
+
+Then, you send the request to the API route. Once the request is resolved successfully, you revalidate the cart cache tag to ensure that the cart's details are updated and refetched by other components. This ensures that the promotion is removed from the checkout summary without needing to refresh the page.
+
+Finally, you'll use this function in the `handleTogglePromotion` function in the `LoyaltyPoints` component you created earlier.
+
+At the top of `src/modules/checkout/components/loyalty-points/index.tsx`, add the following import:
+
+```tsx title="src/modules/checkout/components/loyalty-points/index.tsx" badgeLabel="Storefront" badgeColor="blue"
+import { removeLoyaltyPointsOnCart } from "../../../../lib/data/cart"
+```
+
+Then, replace the `TODO` in `handleTogglePromotion` with the following:
+
+```tsx title="src/modules/checkout/components/loyalty-points/index.tsx" badgeLabel="Storefront" badgeColor="blue"
+await removeLoyaltyPointsOnCart()
+```
+
+In the `handleTogglePromotion` function, you call the `removeLoyaltyPointsOnCart` function if the cart has a loyalty promotion. This will send a request to the API route you created earlier, which will execute the workflow that removes the loyalty promotion from the cart.
+
+### Test it Out
+
+To test out removing the loyalty points from the cart, start the Medusa application and Next.js Starter Storefront.
+
+Then, in the checkout flow as an authenticated customer, after applying the loyalty points, click on the "Remove Loyalty Points" button. The checkout summary will be updated with the removed promotion and the discount amount.
+
+
+
+***
+
+## Step 9: Validate Loyalty Points on Cart Completion
+
+After the customer applies the loyalty points to the cart and places the order, you need to validate that the customer actually has the loyalty points. This prevents edge cases where the customer may have applied the loyalty points previously but they don't have them anymore.
+
+So, in this step, you'll hook into Medusa's cart completion flow to perform the validation.
+
+Since Medusa uses workflows in its API routes, it allows you to hook into them and perform custom functionalities using [Workflow Hooks](https://docs.medusajs.com/docs/learn/fundamentals/workflows/workflow-hooks/index.html.md). A workflow hook is a point in a workflow where you can inject custom functionality as a step function, called a hook handler.
+
+Medusa uses the [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) hook to complete the cart and place an order. This workflow has a `validate` hook that allows you to perform custom validation before the cart is completed.
+
+To consume the `validate` hook, create the file `src/workflows/hooks/complete-cart.ts` with the following content:
+
+```ts title="src/workflows/hooks/complete-cart.ts" highlights={completeCartWorkflowHookHighlights} collapsibleLines="1-6" expandButtonLabel="Show Imports"
+import { completeCartWorkflow } from "@medusajs/medusa/core-flows"
+import LoyaltyModuleService from "../../modules/loyalty/service"
+import { LOYALTY_MODULE } from "../../modules/loyalty"
+import { CartData, getCartLoyaltyPromotion } from "../../utils/promo"
+import { MedusaError } from "@medusajs/framework/utils"
+
+completeCartWorkflow.hooks.validate(
+ async ({ cart }, { container }) => {
+ const query = container.resolve("query")
+ const loyaltyModuleService: LoyaltyModuleService = container.resolve(
+ LOYALTY_MODULE
+ )
+
+ const { data: carts } = await query.graph({
+ entity: "cart",
+ fields: [
+ "id",
+ "promotions.*",
+ "customer.*",
+ "promotions.rules.*",
+ "promotions.rules.values.*",
+ "promotions.application_method.*",
+ "metadata",
+ ],
+ filters: {
+ id: cart.id,
+ },
+ }, {
+ throwIfKeyNotFound: true,
+ })
+
+ const loyaltyPromo = getCartLoyaltyPromotion(
+ carts[0] as unknown as CartData
+ )
+
+ if (!loyaltyPromo) {
+ return
+ }
+
+ const customerLoyaltyPoints = await loyaltyModuleService.getPoints(
+ carts[0].customer!.id
+ )
+ const requiredPoints = await loyaltyModuleService.calculatePointsFromAmount(
+ loyaltyPromo.application_method!.value as number
+ )
+
+ if (customerLoyaltyPoints < requiredPoints) {
+ throw new MedusaError(
+ MedusaError.Types.INVALID_DATA,
+ `Customer does not have enough loyalty points. Required: ${
+ requiredPoints
+ }, Available: ${customerLoyaltyPoints}`
+ )
+ }
+ }
+)
+```
+
+Workflows have a special `hooks` property that includes all the hooks tht you can consume in that workflow. You consume the hook by invoking it from the workflow's `hooks` property.
+
+Since the hook is essentially a step function, it accepts the following parameters:
+
+- The hook's input passed from the workflow, which differs for each hook. The `validate` hook receives an object having the cart's details.
+- The step context object, which contains the Medusa container. You can use it to resolve services and perform actions.
+
+In the hook, you resolve Query and the Loyalty Module's service. Then, you use Query to retrieve the cart's necessary details, including its promotions, customer, and metadata.
+
+After that, you retrieve the customer's loyalty points and calculate the required points to apply the loyalty promotion.
+
+If the customer doesn't have enough loyalty points, you throw an error. This will prevent the cart from being completed if the customer doesn't have enough loyalty points.
+
+***
+
+## Test Out Cart Completion with Loyalty Points
+
+Since you now have the entire loyalty points flow implemented, you can test it out by going through the checkout flow, applying the loyalty points to the cart.
+
+When you place the order, if the customer has sufficient loyalty points, the validation hook will pass.
+
+Then, the `order.placed` event will be emitted, which will execute the subscriber that calls the `handleOrderPointsWorkflow`.
+
+In the workflow, since the order's cart has a loyalty promotion, the points equivalent to the promotion will be deducted, and the promotion becomes inactive.
+
+You can confirm that the loyalty points were deducted either by sending a request to the [retrieve loyalty points API route](#step-5-retrieve-loyalty-points-api-route), or by going through the checkout process again in the storefront.
***
## Next Steps
-You've added support for saved payment methods in your Medusa application and Next.js Starter Storefront, allowing customers to save their payment methods during checkout and use them in future orders.
+You've now implement a loyalty points system in Medusa. There's still more that you can implement based on your use case:
-You can add more features to the saved payment methods, such as allowing customers to delete saved payment methods. You can use [Stripe's APIs](https://docs.stripe.com/api/payment_methods/detach) in the storefront or add an [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) in Medusa to delete the saved payment method.
+- Add loyalty points on registration or other events. Refer to the [Events Reference](https://docs.medusajs.com/references/events/index.html.md) for a full list of available events you can listen to.
+- Show the customer their loyalty point usage history. This will require adding another data model in the Loyalty Module that records the usage history. You can create records of that data model when an order that has a loyalty promotion is placed, then customize the storefront to show a new page for loyalty points history.
+- Customize the Medusa Admin to show a new page or [UI Route](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md) for loyalty points information and analytics.
If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth learning of all the concepts you've used in this guide and more.
@@ -53774,1054 +53932,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).
-# How to Build Magento Data Migration Plugin
-
-In this tutorial, you'll learn how to build a [plugin](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md) that migrates data, such as products, from Magento to Medusa.
-
-Magento is known for its customization capabilities. However, its monolithic architecture imposes limitations on business requirements, often forcing development teams to implement hacky workarounds. Over time, these customizations become challenging to maintain, especially as the business scales, leading to increased technical debt and slower feature delivery.
-
-Medusa's modular architecture allows you to build a custom digital commerce platform that meets your business requirements without the limitations of a monolithic system. By migrating from Magento to Medusa, you can take advantage of Medusa's modern technology stack to build a scalable and flexible commerce platform that grows with your business.
-
-By following this tutorial, you'll create a Medusa plugin that migrates data from a Magento server to a Medusa application in minimal time. You can re-use this plugin across multiple Medusa applications, allowing you to adopt Medusa across your projects.
-
-## Summary
-
-### Prerequisites
-
-
-
-This tutorial will teach you how to:
-
-- Install and set up a Medusa application project.
-- Install and set up a Medusa plugin.
-- Implement a Magento Module in the plugin to connect to Magento's APIs and retrieve products.
- - This guide will only focus on migrating product data from Magento to Medusa. You can extend the implementation to migrate other data, such as customers, orders, and more.
-- Trigger data migration from Magento to Medusa in a scheduled job.
-
-You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer.
-
-
-
-[Example Repository](https://github.com/medusajs/examples/tree/main/migrate-from-magento): Find the full code of the guide in this repository. The repository also includes additional features, such as triggering migrations from the Medusa Admin dashboard.
-
-***
-
-## Step 1: Install a Medusa Application
-
-You'll first install a Medusa application that exposes core commerce features through REST APIs. You'll later install the Magento plugin in this application to test it out.
-
-### Prerequisites
-
-- [Node.js v20+](https://nodejs.org/en/download)
-- [Git CLI tool](https://git-scm.com/downloads)
-- [PostgreSQL](https://www.postgresql.org/download/)
-
-Start by installing the Medusa application on your machine with the following command:
-
-```bash
-npx create-medusa-app@latest
-```
-
-You'll be asked for the project's name. You can also optionally choose to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md).
-
-Afterward, the installation process will start, which will install the Medusa application in a directory with your project's name. If you chose to install the Next.js starter, it'll be installed in a separate directory with the `{project-name}-storefront` name.
-
-The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Refer to the [Medusa Architecture](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md) documentation to learn more.
-
-Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterward, you can log in with the new user and explore the dashboard.
-
-Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.
-
-***
-
-## Step 2: Install a Medusa Plugin Project
-
-A plugin is a package of reusable Medusa customizations that you can install in any Medusa application. You can add in the plugin [API Routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md), [Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), and other customizations, as you'll see in this guide. Afterward, you can test it out locally in a Medusa application, then publish it to npm to install and use it in any Medusa application.
-
-Refer to the [Plugins](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md) documentation to learn more about plugins.
-
-A Medusa plugin is set up in a different project, giving you the flexibility in building and publishing it, while providing you with the tools to test it out locally in a Medusa application.
-
-To create a new Medusa plugin project, run the following command in a directory different than that of the Medusa application:
-
-```bash npm2yarn
-npx create-medusa-app@latest medusa-plugin-magento --plugin
-```
-
-Where `medusa-plugin-magento` is the name of the plugin's directory and the name set in the plugin's `package.json`. So, if you wish to publish it to NPM later under a different name, you can change it here in the command or later in `package.json`.
-
-Once the installation process is done, a new directory named `medusa-plugin-magento` will be created with the plugin project files.
-
-
-
-***
-
-## Step 3: Set up Plugin in Medusa Application
-
-Before you start your development, you'll set up the plugin in the Medusa application you installed in the first step. This will allow you to test the plugin during your development process.
-
-In the plugin's directory, run the following command to publish the plugin to the local package registry:
-
-```bash title="Plugin project"
-npx medusa plugin:publish
-```
-
-This command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. The plugin is published locally under the name you specified in `package.json`.
-
-Next, you'll install the plugin in the Medusa application from the local registry.
-
-If you've installed your Medusa project before v2.3.1, you must install [yalc](https://github.com/wclr/yalc) as a development dependency first.
-
-Run the following command in the Medusa application's directory to install the plugin:
-
-```bash title="Medusa application"
-npx medusa plugin:add medusa-plugin-magento
-```
-
-This command installs the plugin in the Medusa application from the local package registry.
-
-Next, register the plugin in the `medusa-config.ts` file of the Medusa application:
-
-```ts title="medusa-config.ts"
-module.exports = defineConfig({
- // ...
- plugins: [
- {
- resolve: "medusa-plugin-magento",
- options: {
- // TODO add options
- },
- },
- ],
-})
-```
-
-You add the plugin to the array of plugins. Later, you'll pass options useful to retrieve data from Magento.
-
-Finally, to ensure your plugin's changes are constantly published to the local registry, simplifying your testing process, keep the following command running in the plugin project during development:
-
-```bash title="Plugin project"
-npx medusa plugin:develop
-```
-
-***
-
-## Step 4: Implement Magento Module
-
-To connect to external applications in Medusa, you create a custom module. A module is a reusable package with functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup.
-
-In this step, you'll create a Magento Module in the Magento plugin that connects to a Magento server's REST APIs and retrieves data, such as products.
-
-Refer to the [Modules](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) documentation to learn more about modules.
-
-### Create Module Directory
-
-A module is created under the `src/modules` directory of your plugin. So, create the directory `src/modules/magento`.
-
-
-
-### Create Module's Service
-
-You define a module's functionalities in a service. A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to external systems or the database, which is useful if your module defines tables in the database.
-
-In this section, you'll create the Magento Module's service that connects to Magento's REST APIs and retrieves data.
-
-Start by creating the file `src/modules/magento/service.ts` in the plugin with the following content:
-
-
-
-```ts title="src/modules/magento/service.ts"
-type Options = {
- baseUrl: string
- storeCode?: string
- username: string
- password: string
- migrationOptions?: {
- imageBaseUrl?: string
- }
-}
-
-export default class MagentoModuleService {
- private options: Options
-
- constructor({}, options: Options) {
- this.options = {
- ...options,
- storeCode: options.storeCode || "default",
- }
- }
-}
-```
-
-You create a `MagentoModuleService` that has an `options` property to store the module's options. These options include:
-
-- `baseUrl`: The base URL of the Magento server.
-- `storeCode`: The store code of the Magento store, which is `default` by default.
-- `username`: The username of a Magento admin user to authenticate with the Magento server.
-- `password`: The password of the Magento admin user.
-- `migrationOptions`: Additional options useful for migrating data, such as the base URL to use for product images.
-
-The service's constructor accepts as a first parameter the [Module Container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md), which allows you to access resources available for the module. As a second parameter, it accepts the module's options.
-
-### Add Authentication Logic
-
-To authenticate with the Magento server, you'll add a method to the service that retrieves an access token from Magento using the username and password in the options. This access token is used in subsequent requests to the Magento server.
-
-First, add the following property to the `MagentoModuleService` class:
-
-```ts title="src/modules/magento/service.ts"
-export default class MagentoModuleService {
- private accessToken: {
- token: string
- expiresAt: Date
- }
- // ...
-}
-```
-
-You add an `accessToken` property to store the access token and its expiration date. The access token Magento returns expires after four hours, so you store the expiration date to know when to refresh the token.
-
-Next, add the following `authenticate` method to the `MagentoModuleService` class:
-
-```ts title="src/modules/magento/service.ts"
-import { MedusaError } from "@medusajs/framework/utils"
-
-export default class MagentoModuleService {
- // ...
- async authenticate() {
- const response = await fetch(`${this.options.baseUrl}/rest/${this.options.storeCode}/V1/integration/admin/token`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({ username: this.options.username, password: this.options.password }),
- })
-
- const token = await response.text()
-
- if (!response.ok) {
- throw new MedusaError(MedusaError.Types.UNAUTHORIZED, `Failed to authenticate with Magento: ${token}`)
- }
-
- this.accessToken = {
- token: token.replaceAll("\"", ""),
- expiresAt: new Date(Date.now() + 4 * 60 * 60 * 1000), // 4 hours in milliseconds
- }
- }
-}
-```
-
-You create an `authenticate` method that sends a POST request to the Magento server's `/rest/{storeCode}/V1/integration/admin/token` endpoint, passing the username and password in the request body.
-
-If the request is successful, you store the access token and its expiration date in the `accessToken` property. If the request fails, you throw a `MedusaError` with the error message returned by Magento.
-
-Lastly, add an `isAccessTokenExpired` method that checks if the access token has expired:
-
-```ts title="src/modules/magento/service.ts"
-export default class MagentoModuleService {
- // ...
- async isAccessTokenExpired(): Promise {
- return !this.accessToken || this.accessToken.expiresAt < new Date()
- }
-}
-```
-
-In the `isAccessTokenExpired` method, you return a boolean indicating whether the access token has expired. You'll use this in later methods to check if you need to refresh the access token.
-
-### Retrieve Products from Magento
-
-Next, you'll add a method that retrieves products from Magento. Due to limitations in Magento's API that makes it difficult to differentiate between simple products that don't belong to a configurable product and those that do, you'll only retrieve configurable products and their children. You'll also retrieve the configurable attributes of the product, such as color and size.
-
-First, you'll add some types to represent a Magento product and its attributes. Create the file `src/modules/magento/types.ts` in the plugin with the following content:
-
-
-
-```ts title="src/modules/magento/types.ts"
-export type MagentoProduct = {
- id: number
- sku: string
- name: string
- price: number
- status: number
- // not handling other types
- type_id: "simple" | "configurable"
- created_at: string
- updated_at: string
- extension_attributes: {
- category_links: {
- category_id: string
- }[]
- configurable_product_links?: number[]
- configurable_product_options?: {
- id: number
- attribute_id: string
- label: string
- position: number
- values: {
- value_index: number
- }[]
- }[]
- }
- media_gallery_entries: {
- id: number
- media_type: string
- label: string
- position: number
- disabled: boolean
- types: string[]
- file: string
- }[]
- custom_attributes: {
- attribute_code: string
- value: string
- }[]
- // added by module
- children?: MagentoProduct[]
-}
-
-export type MagentoAttribute = {
- attribute_code: string
- attribute_id: number
- default_frontend_label: string
- options: {
- label: string
- value: string
- }[]
-}
-
-export type MagentoPagination = {
- search_criteria: {
- filter_groups: [],
- page_size: number
- current_page: number
- }
- total_count: number
-}
-
-export type MagentoPaginatedResponse = {
- items: TData[]
-} & MagentoPagination
-```
-
-You define the following types:
-
-- `MagentoProduct`: Represents a product in Magento.
-- `MagentoAttribute`: Represents an attribute in Magento.
-- `MagentoPagination`: Represents the pagination information returned by Magento's API.
-- `MagentoPaginatedResponse`: Represents a paginated response from Magento's API for a specific item type, such as products.
-
-Next, add the `getProducts` method to the `MagentoModuleService` class:
-
-```ts title="src/modules/magento/service.ts"
-export default class MagentoModuleService {
- // ...
- async getProducts(options?: {
- currentPage?: number
- pageSize?: number
- }): Promise<{
- products: MagentoProduct[]
- attributes: MagentoAttribute[]
- pagination: MagentoPagination
- }> {
- const { currentPage = 1, pageSize = 100 } = options || {}
- const getAccessToken = await this.isAccessTokenExpired()
- if (getAccessToken) {
- await this.authenticate()
- }
-
- // TODO prepare query params
- }
-}
-```
-
-The `getProducts` method receives an optional `options` object with the `currentPage` and `pageSize` properties. So far, you check if the access token has expired and, if so, retrieve a new one using the `authenticate` method.
-
-Next, you'll prepare the query parameters to pass in the request that retrieves products. Replace the `TODO` with the following:
-
-```ts title="src/modules/magento/service.ts"
-const searchQuery = new URLSearchParams()
-// pass pagination parameters
-searchQuery.append(
- "searchCriteria[currentPage]",
- currentPage?.toString() || "1"
-)
-searchQuery.append(
- "searchCriteria[pageSize]",
- pageSize?.toString() || "100"
-)
-
-// retrieve only configurable products
-searchQuery.append(
- "searchCriteria[filter_groups][1][filters][0][field]",
- "type_id"
-)
-searchQuery.append(
- "searchCriteria[filter_groups][1][filters][0][value]",
- "configurable"
-)
-searchQuery.append(
- "searchCriteria[filter_groups][1][filters][0][condition_type]",
- "in"
-)
-
-// TODO send request to retrieve products
-```
-
-You create a `searchQuery` object to store the query parameters to pass in the request. Then, you add the pagination parameters and the filter to retrieve only configurable products.
-
-Next, you'll send the request to retrieve products from Magento. Replace the `TODO` with the following:
-
-```ts title="src/modules/magento/service.ts"
-const { items: products, ...pagination }: MagentoPaginatedResponse = await fetch(
- `${this.options.baseUrl}/rest/${this.options.storeCode}/V1/products?${searchQuery}`,
- {
- headers: {
- "Authorization": `Bearer ${this.accessToken.token}`,
- },
- }
-).then((res) => res.json())
-.catch((err) => {
- console.log(err)
- throw new MedusaError(
- MedusaError.Types.INVALID_DATA,
- `Failed to get products from Magento: ${err.message}`
- )
-})
-
-// TODO prepare products
-```
-
-You send a `GET` request to the Magento server's `/rest/{storeCode}/V1/products` endpoint, passing the query parameters in the URL. You also pass the access token in the `Authorization` header.
-
-Next, you'll prepare the retrieved products by retrieving their children, configurable attributes, and modifying their image URLs. Replace the `TODO` with the following:
-
-```ts title="src/modules/magento/service.ts"
-const attributeIds: string[] = []
-
-await promiseAll(
- products.map(async (product) => {
- // retrieve its children
- product.children = await fetch(
- `${this.options.baseUrl}/rest/${this.options.storeCode}/V1/configurable-products/${product.sku}/children`,
- {
- headers: {
- "Authorization": `Bearer ${this.accessToken.token}`,
- },
- }
- ).then((res) => res.json())
- .catch((err) => {
- throw new MedusaError(
- MedusaError.Types.INVALID_DATA,
- `Failed to get product children from Magento: ${err.message}`
- )
- })
-
- product.media_gallery_entries = product.media_gallery_entries.map(
- (entry) => ({
- ...entry,
- file: `${this.options.migrationOptions?.imageBaseUrl}${entry.file}`,
- }
- ))
-
- attributeIds.push(...(
- product.extension_attributes.configurable_product_options?.map(
- (option) => option.attribute_id) || []
- )
- )
- })
-)
-
-// TODO retrieve attributes
-```
-
-You loop over the retrieved products and retrieve their children using the `/rest/{storeCode}/V1/configurable-products/{sku}/children` endpoint. You also modify the image URLs to use the base URL in the migration options, if provided.
-
-In addition, you store the IDs of the configurable products' attributes in the `attributeIds` array. You'll add a method that retrieves these attributes.
-
-Add the new method `getAttributes` to the `MagentoModuleService` class:
-
-```ts title="src/modules/magento/service.ts"
-export default class MagentoModuleService {
- // ...
- async getAttributes({
- ids,
- }: {
- ids: string[]
- }): Promise {
- const getAccessToken = await this.isAccessTokenExpired()
- if (getAccessToken) {
- await this.authenticate()
- }
-
- // filter by attribute IDs
- const searchQuery = new URLSearchParams()
- searchQuery.append(
- "searchCriteria[filter_groups][0][filters][0][field]",
- "attribute_id"
- )
- searchQuery.append(
- "searchCriteria[filter_groups][0][filters][0][value]",
- ids.join(",")
- )
- searchQuery.append(
- "searchCriteria[filter_groups][0][filters][0][condition_type]",
- "in"
- )
-
- const {
- items: attributes,
- }: MagentoPaginatedResponse = await fetch(
- `${this.options.baseUrl}/rest/${this.options.storeCode}/V1/products/attributes?${searchQuery}`,
- {
- headers: {
- "Authorization": `Bearer ${this.accessToken.token}`,
- },
- }
- ).then((res) => res.json())
- .catch((err) => {
- throw new MedusaError(
- MedusaError.Types.INVALID_DATA,
- `Failed to get attributes from Magento: ${err.message}`
- )
- })
-
- return attributes
- }
-}
-```
-
-The `getAttributes` method receives an object with the `ids` property, which is an array of attribute IDs. You check if the access token has expired and, if so, retrieve a new one using the `authenticate` method.
-
-Next, you prepare the query parameters to pass in the request to retrieve attributes. You send a `GET` request to the Magento server's `/rest/{storeCode}/V1/products/attributes` endpoint, passing the query parameters in the URL. You also pass the access token in the `Authorization` header.
-
-Finally, you return the retrieved attributes.
-
-Now, go back to the `getProducts` method and replace the `TODO` with the following:
-
-```ts title="src/modules/magento/service.ts"
-const attributes = await this.getAttributes({ ids: attributeIds })
-
-return { products, attributes, pagination }
-```
-
-You retrieve the configurable products' attributes using the `getAttributes` method and return the products, attributes, and pagination information.
-
-You'll use this method in a later step to retrieve products from Magento.
-
-### Export Module Definition
-
-The final piece to a module is its definition, which you export in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its service.
-
-So, create the file `src/modules/magento/index.ts` with the following content:
-
-
-
-```ts title="src/modules/magento/index.ts"
-import { Module } from "@medusajs/framework/utils"
-import MagentoModuleService from "./service"
-
-export const MAGENTO_MODULE = "magento"
-
-export default Module(MAGENTO_MODULE, {
- service: MagentoModuleService,
-})
-```
-
-You use the `Module` function from the Modules SDK to create the module's definition. It accepts two parameters:
-
-1. The module's name, which is `magento`.
-2. An object with a required property `service` indicating the module's service.
-
-You'll later use the module's service to retrieve products from Magento.
-
-### Pass Options to Plugin
-
-As mentioned earlier when you registered the plugin in the Medusa Application's `medusa-config.ts` file, you can pass options to the plugin. These options are then passed to the modules in the plugin.
-
-So, add the following options to the plugin's registration in the `medusa-config.ts` file of the Medusa application:
-
-```ts title="medusa-config.ts"
-module.exports = defineConfig({
- // ...
- plugins: [
- {
- resolve: "medusa-plugin-magento",
- options: {
- baseUrl: process.env.MAGENTO_BASE_URL,
- username: process.env.MAGENTO_USERNAME,
- password: process.env.MAGENTO_PASSWORD,
- migrationOptions: {
- imageBaseUrl: process.env.MAGENTO_IMAGE_BASE_URL,
- },
- },
- },
- ],
-})
-```
-
-You pass the options that you defined in the `MagentoModuleService`. Make sure to also set their environment variables in the `.env` file:
-
-```bash
-MAGENTO_BASE_URL=https://magento.example.com
-MAGENTO_USERNAME=admin
-MAGENTO_PASSWORD=password
-MAGENTO_IMAGE_BASE_URL=https://magento.example.com/pub/media/catalog/product
-```
-
-Where:
-
-- `MAGENTO_BASE_URL`: The base URL of the Magento server. It can also be a local URL, such as `http://localhost:8080`.
-- `MAGENTO_USERNAME`: The username of a Magento admin user to authenticate with the Magento server.
-- `MAGENTO_PASSWORD`: The password of the Magento admin user.
-- `MAGENTO_IMAGE_BASE_URL`: The base URL to use for product images. Magento stores product images in the `pub/media/catalog/product` directory, so you can reference them directly or use a CDN URL. If the URLs of product images in the Medusa server already have a different base URL, you can omit this option.
-
-Medusa supports integrating third-party services, such as [S3](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/file/s3/index.html.md), in a File Module Provider. Refer to the [File Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/file/index.html.md) documentation to find other module providers and how to create a custom provider.
-
-You can now use the Magento Module to migrate data, which you'll do in the next steps.
-
-***
-
-## Step 5: Build Product Migration Workflow
-
-In this section, you'll add the feature to migrate products from Magento to Medusa. To implement this feature, you'll use a workflow.
-
-A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow like you construct a function, but it's a special function that allows you to track its executions' progress, define roll-back logic, and configure other advanced features. Then, you execute the workflow from other customizations, such as in an API route or a scheduled job.
-
-By implementing the migration feature in a workflow, you ensure that the data remains consistent and that the migration process can be rolled back if an error occurs.
-
-Refer to the [Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) documentation to learn more about workflows.
-
-### Workflow Steps
-
-The workflow you'll create will have the following steps:
-
-- [getMagentoProductsStep](#getMagentoProductsStep): Retrieve products from Magento using the Magento Module.
-- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve Medusa store details, which you'll need when creating the products.
-- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve a shipping profile, which you'll associate the created products with.
-- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve Magento products that are already in Medusa to update them, instead of creating them.
-- [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/index.html.md): Create products in the Medusa application.
-- [updateProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductsWorkflow/index.html.md): Update existing products in the Medusa application.
-
-You only need to implement the `getMagentoProductsStep` step, which retrieves the products from Magento. The other steps and workflows are provided by Medusa's `@medusajs/medusa/core-flows` package.
-
-### getMagentoProductsStep
-
-The first step of the workflow retrieves and returns the products from Magento.
-
-In your plugin, create the file `src/workflows/steps/get-magento-products.ts` with the following content:
-
-
-
-```ts title="src/workflows/steps/get-magento-products.ts"
-import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
-import { MAGENTO_MODULE } from "../../modules/magento"
-import MagentoModuleService from "../../modules/magento/service"
-
-type GetMagentoProductsInput = {
- currentPage: number
- pageSize: number
-}
-
-export const getMagentoProductsStep = createStep(
- "get-magento-products",
- async ({ currentPage, pageSize }: GetMagentoProductsInput, { container }) => {
- const magentoModuleService: MagentoModuleService =
- container.resolve(MAGENTO_MODULE)
-
- const response = await magentoModuleService.getProducts({
- currentPage,
- pageSize,
- })
-
- return new StepResponse(response)
- }
-)
-```
-
-You create a step using `createStep` from the Workflows SDK. It accepts two parameters:
-
-1. The step's name, which is `get-magento-products`.
-2. An async function that executes the step's logic. The function receives two parameters:
- - The input data for the step, which in this case is the pagination parameters.
- - An object holding the workflow's context, including the [Medusa Container](https://docs.medusajs.com/docslearn/fundamentals/medusa-container/index.html.md) that allows you to resolve Framework and commerce tools.
-
-In the step function, you resolve the Magento Module's service from the container, then use its `getProducts` method to retrieve the products from Magento.
-
-Steps that return data must return them in a `StepResponse` instance. The `StepResponse` constructor accepts as a parameter the data to return.
-
-### Create migrateProductsFromMagentoWorkflow
-
-You'll now create the workflow that migrates products from Magento using the step you created and steps from Medusa's `@medusajs/medusa/core-flows` package.
-
-In your plugin, create the file `src/workflows/migrate-products-from-magento.ts` with the following content:
-
-
-
-```ts title="src/workflows/migrate-products-from-magento.ts"
-import {
- createWorkflow, transform, WorkflowResponse,
-} from "@medusajs/framework/workflows-sdk"
-import {
- CreateProductWorkflowInputDTO, UpsertProductDTO,
-} from "@medusajs/framework/types"
-import {
- createProductsWorkflow,
- updateProductsWorkflow,
- useQueryGraphStep,
-} from "@medusajs/medusa/core-flows"
-import { getMagentoProductsStep } from "./steps/get-magento-products"
-
-type MigrateProductsFromMagentoWorkflowInput = {
- currentPage: number
- pageSize: number
-}
-
-export const migrateProductsFromMagentoWorkflowId =
- "migrate-products-from-magento"
-
-export const migrateProductsFromMagentoWorkflow = createWorkflow(
- {
- name: migrateProductsFromMagentoWorkflowId,
- retentionTime: 10000,
- store: true,
- },
- (input: MigrateProductsFromMagentoWorkflowInput) => {
- const { pagination, products, attributes } = getMagentoProductsStep(
- input
- )
- // TODO prepare data to create and update products
- }
-)
-```
-
-You create a workflow using `createWorkflow` from the Workflows SDK. It accepts two parameters:
-
-1. An object with the workflow's configuration, including the name and whether to store the workflow's executions. You enable storing the workflow execution so that you can view it later in the Medusa Admin dashboard.
-2. A worflow constructor function, which holds the workflow's implementation. The function receives the input data for the workflow, which is the pagination parameters.
-
-In the workflow constructor function, you use the `getMagentoProductsStep` step to retrieve the products from Magento, passing it the pagination parameters from the workflow's input.
-
-Next, you'll retrieve the Medusa store details and shipping profiles. These are necessary to prepare the data of the products to create or update.
-
-Replace the `TODO` in the workflow function with the following:
-
-```ts title="src/workflows/migrate-products-from-magento.ts"
-const { data: stores } = useQueryGraphStep({
- entity: "store",
- fields: ["supported_currencies.*", "default_sales_channel_id"],
- pagination: {
- take: 1,
- skip: 0,
- },
-})
-
-const { data: shippingProfiles } = useQueryGraphStep({
- entity: "shipping_profile",
- fields: ["id"],
- pagination: {
- take: 1,
- skip: 0,
- },
-}).config({ name: "get-shipping-profiles" })
-
-// TODO retrieve existing products
-```
-
-You use the `useQueryGraphStep` step to retrieve the store details and shipping profiles. `useQueryGraphStep` is a Medusa step that wraps [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), allowing you to use it in a workflow. Query is a tool that retrieves data across modules.
-
-Whe retrieving the store details, you specifically retrieve its supported currencies and default sales channel ID. You'll associate the products with the store's default sales channel, and set their variant prices in the supported currencies. You'll also associate the products with a shipping profile.
-
-Next, you'll retrieve products that were previously migrated from Magento to determine which products to create or update. Replace the `TODO` with the following:
-
-```ts title="src/workflows/migrate-products-from-magento.ts"
-const externalIdFilters = transform({
- products,
-}, (data) => {
- return data.products.map((product) => product.id.toString())
-})
-
-const { data: existingProducts } = useQueryGraphStep({
- entity: "product",
- fields: ["id", "external_id", "variants.id", "variants.metadata"],
- filters: {
- external_id: externalIdFilters,
- },
-}).config({ name: "get-existing-products" })
-
-// TODO prepare products to create or update
-```
-
-Since the Medusa application creates an internal representation of the workflow's constructor function, you can't manipulate data directly, as variables have no value while creating the internal representation.
-
-Refer to the [Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md) documentation to learn more about the workflow constructor function's constraints.
-
-Instead, you can manipulate data in a workflow's constructor function using `transform` from the Workflows SDK. `transform` is a function that accepts two parameters:
-
-- The data to transform, which in this case is the Magento products.
-- A function that transforms the data. The function receives the data passed in the first parameter and returns the transformed data.
-
-In the transformation function, you return the IDs of the Magento products. Then, you use the `useQueryGraphStep` to retrieve products in the Medusa application that have an `external_id` property matching the IDs of the Magento products. You'll use this property to store the IDs of the products in Magento.
-
-Next, you'll prepare the data to create and update the products. Replace the `TODO` in the workflow function with the following:
-
-```ts title="src/workflows/migrate-products-from-magento.ts" highlights={prepareHighlights}
-const {
- productsToCreate,
- productsToUpdate,
-} = transform({
- products,
- attributes,
- stores,
- shippingProfiles,
- existingProducts,
-}, (data) => {
- const productsToCreate = new Map()
- const productsToUpdate = new Map()
-
- data.products.forEach((magentoProduct) => {
- const productData: CreateProductWorkflowInputDTO | UpsertProductDTO = {
- title: magentoProduct.name,
- description: magentoProduct.custom_attributes.find(
- (attr) => attr.attribute_code === "description"
- )?.value,
- status: magentoProduct.status === 1 ? "published" : "draft",
- handle: magentoProduct.custom_attributes.find(
- (attr) => attr.attribute_code === "url_key"
- )?.value,
- external_id: magentoProduct.id.toString(),
- thumbnail: magentoProduct.media_gallery_entries.find(
- (entry) => entry.types.includes("thumbnail")
- )?.file,
- sales_channels: [{
- id: data.stores[0].default_sales_channel_id,
- }],
- shipping_profile_id: data.shippingProfiles[0].id,
- }
- const existingProduct = data.existingProducts.find((p) => p.external_id === productData.external_id)
-
- if (existingProduct) {
- productData.id = existingProduct.id
- }
-
- productData.options = magentoProduct.extension_attributes.configurable_product_options?.map((option) => {
- const attribute = data.attributes.find((attr) => attr.attribute_id === parseInt(option.attribute_id))
- return {
- title: option.label,
- values: attribute?.options.filter((opt) => {
- return option.values.find((v) => v.value_index === parseInt(opt.value))
- }).map((opt) => opt.label) || [],
- }
- }) || []
-
- productData.variants = magentoProduct.children?.map((child) => {
- const childOptions: Record = {}
-
- child.custom_attributes.forEach((attr) => {
- const attrData = data.attributes.find((a) => a.attribute_code === attr.attribute_code)
- if (!attrData) {
- return
- }
-
- childOptions[attrData.default_frontend_label] = attrData.options.find((opt) => opt.value === attr.value)?.label || ""
- })
-
- const variantExternalId = child.id.toString()
- const existingVariant = existingProduct.variants.find((v) => v.metadata.external_id === variantExternalId)
-
- return {
- title: child.name,
- sku: child.sku,
- options: childOptions,
- prices: data.stores[0].supported_currencies.map(({ currency_code }) => {
- return {
- amount: child.price,
- currency_code,
- }
- }),
- metadata: {
- external_id: variantExternalId,
- },
- id: existingVariant?.id,
- }
- })
-
- productData.images = magentoProduct.media_gallery_entries.filter((entry) => !entry.types.includes("thumbnail")).map((entry) => {
- return {
- url: entry.file,
- metadata: {
- external_id: entry.id.toString(),
- },
- }
- })
-
- if (productData.id) {
- productsToUpdate.set(existingProduct.id, productData)
- } else {
- productsToCreate.set(productData.external_id!, productData)
- }
- })
-
- return {
- productsToCreate: Array.from(productsToCreate.values()),
- productsToUpdate: Array.from(productsToUpdate.values()),
- }
-})
-
-// TODO create and update products
-```
-
-You use `transform` again to prepare the data to create and update the products in the Medusa application. For each Magento product, you map its equivalent Medusa product's data:
-
-- You set the product's general details, such as the title, description, status, handle, external ID, and thumbnail using the Magento product's data and custom attributes.
-- You associate the product with the default sales channel and shipping profile retrieved previously.
-- You map the Magento product's configurable product options to Medusa product options. In Medusa, a product's option has a label, such as "Color", and values, such as "Red". To map the option values, you use the attributes retrieved from Magento.
-- You map the Magento product's children to Medusa product variants. For the variant options, you pass an object whose keys is the option's label, such as "Color", and values is the option's value, such as "Red". For the prices, you set the variant's price based on the Magento child's price for every supported currency in the Medusa store. Also, you set the Magento child product's ID in the Medusa variant's `metadata.external_id` property.
-- You map the Magento product's media gallery entries to Medusa product images. You filter out the thumbnail image and set the URL and the Magento image's ID in the Medusa image's `metadata.external_id` property.
-
-In addition, you use the existing products retrieved in the previous step to determine whether a product should be created or updated. If there's an existing product whose `external_id` matches the ID of the magento product, you set the existing product's ID in the `id` property of the product to be updated. You also do the same for its variants.
-
-Finally, you return the products to create and update.
-
-The last steps of the workflow is to create and update the products. Replace the `TODO` in the workflow function with the following:
-
-```ts title="src/workflows/migrate-products-from-magento.ts"
-createProductsWorkflow.runAsStep({
- input: {
- products: productsToCreate,
- },
-})
-
-updateProductsWorkflow.runAsStep({
- input: {
- products: productsToUpdate,
- },
-})
-
-return new WorkflowResponse(pagination)
-```
-
-You use the `createProductsWorkflow` and `updateProductsWorkflow` workflows from Medusa's `@medusajs/medusa/core-flows` package to create and update the products in the Medusa application.
-
-Workflows must return an instance of `WorkflowResponse`, passing as a parameter the data to return to the workflow's executor. This workflow returns the pagination parameters, allowing you to paginate the product migration process.
-
-You can now use this workflow to migrate products from Magento to Medusa. You'll learn how to use it in the next steps.
-
-***
-
-## Step 6: Schedule Product Migration
-
-There are many ways to execute tasks asynchronously in Medusa, such as [scheduling a job](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md) or [handling emitted events](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md).
-
-In this guide, you'll learn how to schedule the product migration at a specified interval using a scheduled job. A scheduled job is an asynchronous function that the Medusa application runs at the interval you specify during the Medusa application's runtime.
-
-Refer to the [Scheduled Jobs](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md) documentation to learn more about scheduled jobs.
-
-To create a scheduled job, in your plugin, create the file `src/jobs/migrate-magento.ts` with the following content:
-
-
-
-```ts title="src/jobs/migrate-magento.ts"
-import { MedusaContainer } from "@medusajs/framework/types"
-import { migrateProductsFromMagentoWorkflow } from "../workflows"
-
-export default async function migrateMagentoJob(
- container: MedusaContainer
-) {
- const logger = container.resolve("logger")
- logger.info("Migrating products from Magento...")
-
- let currentPage = 0
- const pageSize = 100
- let totalCount = 0
-
- do {
- currentPage++
-
- const {
- result: pagination,
- } = await migrateProductsFromMagentoWorkflow(container).run({
- input: {
- currentPage,
- pageSize,
- },
- })
-
- totalCount = pagination.total_count
- } while (currentPage * pageSize < totalCount)
-
- logger.info("Finished migrating products from Magento")
-}
-
-export const config = {
- name: "migrate-magento-job",
- schedule: "0 0 * * *",
-}
-```
-
-A scheduled job file must export:
-
-- An asynchronous function that executes the job's logic. The function receives the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) as a parameter.
-- An object with the job's configuration, including the name and the schedule. The schedule is a cron job pattern as a string.
-
-In the job function, you resolve the [logger](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md) from the container to log messages. Then, you paginate the product migration process by running the `migrateProductsFromMagentoWorkflow` workflow at each page until you've migrated all products. You use the pagination result returned by the workflow to determine whether there are more products to migrate.
-
-Based on the job's configurations, the Medusa application will run the job at midnight every day.
-
-### Test it Out
-
-To test out this scheduled job, first, change the configuration to run the job every minute:
-
-```ts title="src/jobs/migrate-magento.ts"
-export const config = {
- // ...
- schedule: "* * * * *",
-}
-```
-
-Then, make sure to run the `plugin:develop` command in the plugin if you haven't already:
-
-```bash
-npx medusa plugin:develop
-```
-
-This ensures that the plugin's latest changes are reflected in the Medusa application.
-
-Finally, start the Medusa application that the plugin is installed in:
-
-```bash npm2yarn
-npm run dev
-```
-
-After a minute, you'll see a message in the terminal indicating that the migration started:
-
-```plain title="Terminal"
-info: Migrating products from Magento...
-```
-
-Once the migration is done, you'll see the following message:
-
-```plain title="Terminal"
-info: Finished migrating products from Magento
-```
-
-To confirm that the products were migrated, open the Medusa Admin dashboard at `http://localhost:9000/app` and log in. Then, click on Products in the sidebar. You'll see your magento products in the list of products.
-
-
-
-***
-
-## Next Steps
-
-You've now implemented the logic to migrate products from Magento to Medusa. You can re-use the plugin across Medusa applications. You can also expand on the plugin to:
-
-- Migrate other entities, such as orders, customers, and categories. Migrating other entities follows the same pattern as migrating products, using workflows and scheduled jobs. You only need to format the data to be migrated as needed.
-- Allow triggering migrations from the Medusa Admin dashboard using [Admin Customizations](https://docs.medusajs.com/docs/learn/fundamentals/admin/index.html.md). This feature is available in the [Example Repository](https://github.com/medusajs/example-repository/tree/main/src/admin).
-
-If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth learning of all the concepts you've used in this guide and more.
-
-To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).
-
-
# Integrate Medusa with ShipStation (Fulfillment)
In this guide, you'll learn how to integrate Medusa with ShipStation.
@@ -56061,6 +55171,1054 @@ If you're new to Medusa, check out the [main documentation](https://docs.medusaj
To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).
+# How to Build Magento Data Migration Plugin
+
+In this tutorial, you'll learn how to build a [plugin](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md) that migrates data, such as products, from Magento to Medusa.
+
+Magento is known for its customization capabilities. However, its monolithic architecture imposes limitations on business requirements, often forcing development teams to implement hacky workarounds. Over time, these customizations become challenging to maintain, especially as the business scales, leading to increased technical debt and slower feature delivery.
+
+Medusa's modular architecture allows you to build a custom digital commerce platform that meets your business requirements without the limitations of a monolithic system. By migrating from Magento to Medusa, you can take advantage of Medusa's modern technology stack to build a scalable and flexible commerce platform that grows with your business.
+
+By following this tutorial, you'll create a Medusa plugin that migrates data from a Magento server to a Medusa application in minimal time. You can re-use this plugin across multiple Medusa applications, allowing you to adopt Medusa across your projects.
+
+## Summary
+
+### Prerequisites
+
+
+
+This tutorial will teach you how to:
+
+- Install and set up a Medusa application project.
+- Install and set up a Medusa plugin.
+- Implement a Magento Module in the plugin to connect to Magento's APIs and retrieve products.
+ - This guide will only focus on migrating product data from Magento to Medusa. You can extend the implementation to migrate other data, such as customers, orders, and more.
+- Trigger data migration from Magento to Medusa in a scheduled job.
+
+You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer.
+
+
+
+[Example Repository](https://github.com/medusajs/examples/tree/main/migrate-from-magento): Find the full code of the guide in this repository. The repository also includes additional features, such as triggering migrations from the Medusa Admin dashboard.
+
+***
+
+## Step 1: Install a Medusa Application
+
+You'll first install a Medusa application that exposes core commerce features through REST APIs. You'll later install the Magento plugin in this application to test it out.
+
+### Prerequisites
+
+- [Node.js v20+](https://nodejs.org/en/download)
+- [Git CLI tool](https://git-scm.com/downloads)
+- [PostgreSQL](https://www.postgresql.org/download/)
+
+Start by installing the Medusa application on your machine with the following command:
+
+```bash
+npx create-medusa-app@latest
+```
+
+You'll be asked for the project's name. You can also optionally choose to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md).
+
+Afterward, the installation process will start, which will install the Medusa application in a directory with your project's name. If you chose to install the Next.js starter, it'll be installed in a separate directory with the `{project-name}-storefront` name.
+
+The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Refer to the [Medusa Architecture](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md) documentation to learn more.
+
+Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterward, you can log in with the new user and explore the dashboard.
+
+Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.
+
+***
+
+## Step 2: Install a Medusa Plugin Project
+
+A plugin is a package of reusable Medusa customizations that you can install in any Medusa application. You can add in the plugin [API Routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md), [Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), and other customizations, as you'll see in this guide. Afterward, you can test it out locally in a Medusa application, then publish it to npm to install and use it in any Medusa application.
+
+Refer to the [Plugins](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md) documentation to learn more about plugins.
+
+A Medusa plugin is set up in a different project, giving you the flexibility in building and publishing it, while providing you with the tools to test it out locally in a Medusa application.
+
+To create a new Medusa plugin project, run the following command in a directory different than that of the Medusa application:
+
+```bash npm2yarn
+npx create-medusa-app@latest medusa-plugin-magento --plugin
+```
+
+Where `medusa-plugin-magento` is the name of the plugin's directory and the name set in the plugin's `package.json`. So, if you wish to publish it to NPM later under a different name, you can change it here in the command or later in `package.json`.
+
+Once the installation process is done, a new directory named `medusa-plugin-magento` will be created with the plugin project files.
+
+
+
+***
+
+## Step 3: Set up Plugin in Medusa Application
+
+Before you start your development, you'll set up the plugin in the Medusa application you installed in the first step. This will allow you to test the plugin during your development process.
+
+In the plugin's directory, run the following command to publish the plugin to the local package registry:
+
+```bash title="Plugin project"
+npx medusa plugin:publish
+```
+
+This command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. The plugin is published locally under the name you specified in `package.json`.
+
+Next, you'll install the plugin in the Medusa application from the local registry.
+
+If you've installed your Medusa project before v2.3.1, you must install [yalc](https://github.com/wclr/yalc) as a development dependency first.
+
+Run the following command in the Medusa application's directory to install the plugin:
+
+```bash title="Medusa application"
+npx medusa plugin:add medusa-plugin-magento
+```
+
+This command installs the plugin in the Medusa application from the local package registry.
+
+Next, register the plugin in the `medusa-config.ts` file of the Medusa application:
+
+```ts title="medusa-config.ts"
+module.exports = defineConfig({
+ // ...
+ plugins: [
+ {
+ resolve: "medusa-plugin-magento",
+ options: {
+ // TODO add options
+ },
+ },
+ ],
+})
+```
+
+You add the plugin to the array of plugins. Later, you'll pass options useful to retrieve data from Magento.
+
+Finally, to ensure your plugin's changes are constantly published to the local registry, simplifying your testing process, keep the following command running in the plugin project during development:
+
+```bash title="Plugin project"
+npx medusa plugin:develop
+```
+
+***
+
+## Step 4: Implement Magento Module
+
+To connect to external applications in Medusa, you create a custom module. A module is a reusable package with functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup.
+
+In this step, you'll create a Magento Module in the Magento plugin that connects to a Magento server's REST APIs and retrieves data, such as products.
+
+Refer to the [Modules](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) documentation to learn more about modules.
+
+### Create Module Directory
+
+A module is created under the `src/modules` directory of your plugin. So, create the directory `src/modules/magento`.
+
+
+
+### Create Module's Service
+
+You define a module's functionalities in a service. A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to external systems or the database, which is useful if your module defines tables in the database.
+
+In this section, you'll create the Magento Module's service that connects to Magento's REST APIs and retrieves data.
+
+Start by creating the file `src/modules/magento/service.ts` in the plugin with the following content:
+
+
+
+```ts title="src/modules/magento/service.ts"
+type Options = {
+ baseUrl: string
+ storeCode?: string
+ username: string
+ password: string
+ migrationOptions?: {
+ imageBaseUrl?: string
+ }
+}
+
+export default class MagentoModuleService {
+ private options: Options
+
+ constructor({}, options: Options) {
+ this.options = {
+ ...options,
+ storeCode: options.storeCode || "default",
+ }
+ }
+}
+```
+
+You create a `MagentoModuleService` that has an `options` property to store the module's options. These options include:
+
+- `baseUrl`: The base URL of the Magento server.
+- `storeCode`: The store code of the Magento store, which is `default` by default.
+- `username`: The username of a Magento admin user to authenticate with the Magento server.
+- `password`: The password of the Magento admin user.
+- `migrationOptions`: Additional options useful for migrating data, such as the base URL to use for product images.
+
+The service's constructor accepts as a first parameter the [Module Container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md), which allows you to access resources available for the module. As a second parameter, it accepts the module's options.
+
+### Add Authentication Logic
+
+To authenticate with the Magento server, you'll add a method to the service that retrieves an access token from Magento using the username and password in the options. This access token is used in subsequent requests to the Magento server.
+
+First, add the following property to the `MagentoModuleService` class:
+
+```ts title="src/modules/magento/service.ts"
+export default class MagentoModuleService {
+ private accessToken: {
+ token: string
+ expiresAt: Date
+ }
+ // ...
+}
+```
+
+You add an `accessToken` property to store the access token and its expiration date. The access token Magento returns expires after four hours, so you store the expiration date to know when to refresh the token.
+
+Next, add the following `authenticate` method to the `MagentoModuleService` class:
+
+```ts title="src/modules/magento/service.ts"
+import { MedusaError } from "@medusajs/framework/utils"
+
+export default class MagentoModuleService {
+ // ...
+ async authenticate() {
+ const response = await fetch(`${this.options.baseUrl}/rest/${this.options.storeCode}/V1/integration/admin/token`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ username: this.options.username, password: this.options.password }),
+ })
+
+ const token = await response.text()
+
+ if (!response.ok) {
+ throw new MedusaError(MedusaError.Types.UNAUTHORIZED, `Failed to authenticate with Magento: ${token}`)
+ }
+
+ this.accessToken = {
+ token: token.replaceAll("\"", ""),
+ expiresAt: new Date(Date.now() + 4 * 60 * 60 * 1000), // 4 hours in milliseconds
+ }
+ }
+}
+```
+
+You create an `authenticate` method that sends a POST request to the Magento server's `/rest/{storeCode}/V1/integration/admin/token` endpoint, passing the username and password in the request body.
+
+If the request is successful, you store the access token and its expiration date in the `accessToken` property. If the request fails, you throw a `MedusaError` with the error message returned by Magento.
+
+Lastly, add an `isAccessTokenExpired` method that checks if the access token has expired:
+
+```ts title="src/modules/magento/service.ts"
+export default class MagentoModuleService {
+ // ...
+ async isAccessTokenExpired(): Promise {
+ return !this.accessToken || this.accessToken.expiresAt < new Date()
+ }
+}
+```
+
+In the `isAccessTokenExpired` method, you return a boolean indicating whether the access token has expired. You'll use this in later methods to check if you need to refresh the access token.
+
+### Retrieve Products from Magento
+
+Next, you'll add a method that retrieves products from Magento. Due to limitations in Magento's API that makes it difficult to differentiate between simple products that don't belong to a configurable product and those that do, you'll only retrieve configurable products and their children. You'll also retrieve the configurable attributes of the product, such as color and size.
+
+First, you'll add some types to represent a Magento product and its attributes. Create the file `src/modules/magento/types.ts` in the plugin with the following content:
+
+
+
+```ts title="src/modules/magento/types.ts"
+export type MagentoProduct = {
+ id: number
+ sku: string
+ name: string
+ price: number
+ status: number
+ // not handling other types
+ type_id: "simple" | "configurable"
+ created_at: string
+ updated_at: string
+ extension_attributes: {
+ category_links: {
+ category_id: string
+ }[]
+ configurable_product_links?: number[]
+ configurable_product_options?: {
+ id: number
+ attribute_id: string
+ label: string
+ position: number
+ values: {
+ value_index: number
+ }[]
+ }[]
+ }
+ media_gallery_entries: {
+ id: number
+ media_type: string
+ label: string
+ position: number
+ disabled: boolean
+ types: string[]
+ file: string
+ }[]
+ custom_attributes: {
+ attribute_code: string
+ value: string
+ }[]
+ // added by module
+ children?: MagentoProduct[]
+}
+
+export type MagentoAttribute = {
+ attribute_code: string
+ attribute_id: number
+ default_frontend_label: string
+ options: {
+ label: string
+ value: string
+ }[]
+}
+
+export type MagentoPagination = {
+ search_criteria: {
+ filter_groups: [],
+ page_size: number
+ current_page: number
+ }
+ total_count: number
+}
+
+export type MagentoPaginatedResponse = {
+ items: TData[]
+} & MagentoPagination
+```
+
+You define the following types:
+
+- `MagentoProduct`: Represents a product in Magento.
+- `MagentoAttribute`: Represents an attribute in Magento.
+- `MagentoPagination`: Represents the pagination information returned by Magento's API.
+- `MagentoPaginatedResponse`: Represents a paginated response from Magento's API for a specific item type, such as products.
+
+Next, add the `getProducts` method to the `MagentoModuleService` class:
+
+```ts title="src/modules/magento/service.ts"
+export default class MagentoModuleService {
+ // ...
+ async getProducts(options?: {
+ currentPage?: number
+ pageSize?: number
+ }): Promise<{
+ products: MagentoProduct[]
+ attributes: MagentoAttribute[]
+ pagination: MagentoPagination
+ }> {
+ const { currentPage = 1, pageSize = 100 } = options || {}
+ const getAccessToken = await this.isAccessTokenExpired()
+ if (getAccessToken) {
+ await this.authenticate()
+ }
+
+ // TODO prepare query params
+ }
+}
+```
+
+The `getProducts` method receives an optional `options` object with the `currentPage` and `pageSize` properties. So far, you check if the access token has expired and, if so, retrieve a new one using the `authenticate` method.
+
+Next, you'll prepare the query parameters to pass in the request that retrieves products. Replace the `TODO` with the following:
+
+```ts title="src/modules/magento/service.ts"
+const searchQuery = new URLSearchParams()
+// pass pagination parameters
+searchQuery.append(
+ "searchCriteria[currentPage]",
+ currentPage?.toString() || "1"
+)
+searchQuery.append(
+ "searchCriteria[pageSize]",
+ pageSize?.toString() || "100"
+)
+
+// retrieve only configurable products
+searchQuery.append(
+ "searchCriteria[filter_groups][1][filters][0][field]",
+ "type_id"
+)
+searchQuery.append(
+ "searchCriteria[filter_groups][1][filters][0][value]",
+ "configurable"
+)
+searchQuery.append(
+ "searchCriteria[filter_groups][1][filters][0][condition_type]",
+ "in"
+)
+
+// TODO send request to retrieve products
+```
+
+You create a `searchQuery` object to store the query parameters to pass in the request. Then, you add the pagination parameters and the filter to retrieve only configurable products.
+
+Next, you'll send the request to retrieve products from Magento. Replace the `TODO` with the following:
+
+```ts title="src/modules/magento/service.ts"
+const { items: products, ...pagination }: MagentoPaginatedResponse = await fetch(
+ `${this.options.baseUrl}/rest/${this.options.storeCode}/V1/products?${searchQuery}`,
+ {
+ headers: {
+ "Authorization": `Bearer ${this.accessToken.token}`,
+ },
+ }
+).then((res) => res.json())
+.catch((err) => {
+ console.log(err)
+ throw new MedusaError(
+ MedusaError.Types.INVALID_DATA,
+ `Failed to get products from Magento: ${err.message}`
+ )
+})
+
+// TODO prepare products
+```
+
+You send a `GET` request to the Magento server's `/rest/{storeCode}/V1/products` endpoint, passing the query parameters in the URL. You also pass the access token in the `Authorization` header.
+
+Next, you'll prepare the retrieved products by retrieving their children, configurable attributes, and modifying their image URLs. Replace the `TODO` with the following:
+
+```ts title="src/modules/magento/service.ts"
+const attributeIds: string[] = []
+
+await promiseAll(
+ products.map(async (product) => {
+ // retrieve its children
+ product.children = await fetch(
+ `${this.options.baseUrl}/rest/${this.options.storeCode}/V1/configurable-products/${product.sku}/children`,
+ {
+ headers: {
+ "Authorization": `Bearer ${this.accessToken.token}`,
+ },
+ }
+ ).then((res) => res.json())
+ .catch((err) => {
+ throw new MedusaError(
+ MedusaError.Types.INVALID_DATA,
+ `Failed to get product children from Magento: ${err.message}`
+ )
+ })
+
+ product.media_gallery_entries = product.media_gallery_entries.map(
+ (entry) => ({
+ ...entry,
+ file: `${this.options.migrationOptions?.imageBaseUrl}${entry.file}`,
+ }
+ ))
+
+ attributeIds.push(...(
+ product.extension_attributes.configurable_product_options?.map(
+ (option) => option.attribute_id) || []
+ )
+ )
+ })
+)
+
+// TODO retrieve attributes
+```
+
+You loop over the retrieved products and retrieve their children using the `/rest/{storeCode}/V1/configurable-products/{sku}/children` endpoint. You also modify the image URLs to use the base URL in the migration options, if provided.
+
+In addition, you store the IDs of the configurable products' attributes in the `attributeIds` array. You'll add a method that retrieves these attributes.
+
+Add the new method `getAttributes` to the `MagentoModuleService` class:
+
+```ts title="src/modules/magento/service.ts"
+export default class MagentoModuleService {
+ // ...
+ async getAttributes({
+ ids,
+ }: {
+ ids: string[]
+ }): Promise {
+ const getAccessToken = await this.isAccessTokenExpired()
+ if (getAccessToken) {
+ await this.authenticate()
+ }
+
+ // filter by attribute IDs
+ const searchQuery = new URLSearchParams()
+ searchQuery.append(
+ "searchCriteria[filter_groups][0][filters][0][field]",
+ "attribute_id"
+ )
+ searchQuery.append(
+ "searchCriteria[filter_groups][0][filters][0][value]",
+ ids.join(",")
+ )
+ searchQuery.append(
+ "searchCriteria[filter_groups][0][filters][0][condition_type]",
+ "in"
+ )
+
+ const {
+ items: attributes,
+ }: MagentoPaginatedResponse = await fetch(
+ `${this.options.baseUrl}/rest/${this.options.storeCode}/V1/products/attributes?${searchQuery}`,
+ {
+ headers: {
+ "Authorization": `Bearer ${this.accessToken.token}`,
+ },
+ }
+ ).then((res) => res.json())
+ .catch((err) => {
+ throw new MedusaError(
+ MedusaError.Types.INVALID_DATA,
+ `Failed to get attributes from Magento: ${err.message}`
+ )
+ })
+
+ return attributes
+ }
+}
+```
+
+The `getAttributes` method receives an object with the `ids` property, which is an array of attribute IDs. You check if the access token has expired and, if so, retrieve a new one using the `authenticate` method.
+
+Next, you prepare the query parameters to pass in the request to retrieve attributes. You send a `GET` request to the Magento server's `/rest/{storeCode}/V1/products/attributes` endpoint, passing the query parameters in the URL. You also pass the access token in the `Authorization` header.
+
+Finally, you return the retrieved attributes.
+
+Now, go back to the `getProducts` method and replace the `TODO` with the following:
+
+```ts title="src/modules/magento/service.ts"
+const attributes = await this.getAttributes({ ids: attributeIds })
+
+return { products, attributes, pagination }
+```
+
+You retrieve the configurable products' attributes using the `getAttributes` method and return the products, attributes, and pagination information.
+
+You'll use this method in a later step to retrieve products from Magento.
+
+### Export Module Definition
+
+The final piece to a module is its definition, which you export in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its service.
+
+So, create the file `src/modules/magento/index.ts` with the following content:
+
+
+
+```ts title="src/modules/magento/index.ts"
+import { Module } from "@medusajs/framework/utils"
+import MagentoModuleService from "./service"
+
+export const MAGENTO_MODULE = "magento"
+
+export default Module(MAGENTO_MODULE, {
+ service: MagentoModuleService,
+})
+```
+
+You use the `Module` function from the Modules SDK to create the module's definition. It accepts two parameters:
+
+1. The module's name, which is `magento`.
+2. An object with a required property `service` indicating the module's service.
+
+You'll later use the module's service to retrieve products from Magento.
+
+### Pass Options to Plugin
+
+As mentioned earlier when you registered the plugin in the Medusa Application's `medusa-config.ts` file, you can pass options to the plugin. These options are then passed to the modules in the plugin.
+
+So, add the following options to the plugin's registration in the `medusa-config.ts` file of the Medusa application:
+
+```ts title="medusa-config.ts"
+module.exports = defineConfig({
+ // ...
+ plugins: [
+ {
+ resolve: "medusa-plugin-magento",
+ options: {
+ baseUrl: process.env.MAGENTO_BASE_URL,
+ username: process.env.MAGENTO_USERNAME,
+ password: process.env.MAGENTO_PASSWORD,
+ migrationOptions: {
+ imageBaseUrl: process.env.MAGENTO_IMAGE_BASE_URL,
+ },
+ },
+ },
+ ],
+})
+```
+
+You pass the options that you defined in the `MagentoModuleService`. Make sure to also set their environment variables in the `.env` file:
+
+```bash
+MAGENTO_BASE_URL=https://magento.example.com
+MAGENTO_USERNAME=admin
+MAGENTO_PASSWORD=password
+MAGENTO_IMAGE_BASE_URL=https://magento.example.com/pub/media/catalog/product
+```
+
+Where:
+
+- `MAGENTO_BASE_URL`: The base URL of the Magento server. It can also be a local URL, such as `http://localhost:8080`.
+- `MAGENTO_USERNAME`: The username of a Magento admin user to authenticate with the Magento server.
+- `MAGENTO_PASSWORD`: The password of the Magento admin user.
+- `MAGENTO_IMAGE_BASE_URL`: The base URL to use for product images. Magento stores product images in the `pub/media/catalog/product` directory, so you can reference them directly or use a CDN URL. If the URLs of product images in the Medusa server already have a different base URL, you can omit this option.
+
+Medusa supports integrating third-party services, such as [S3](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/file/s3/index.html.md), in a File Module Provider. Refer to the [File Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/file/index.html.md) documentation to find other module providers and how to create a custom provider.
+
+You can now use the Magento Module to migrate data, which you'll do in the next steps.
+
+***
+
+## Step 5: Build Product Migration Workflow
+
+In this section, you'll add the feature to migrate products from Magento to Medusa. To implement this feature, you'll use a workflow.
+
+A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow like you construct a function, but it's a special function that allows you to track its executions' progress, define roll-back logic, and configure other advanced features. Then, you execute the workflow from other customizations, such as in an API route or a scheduled job.
+
+By implementing the migration feature in a workflow, you ensure that the data remains consistent and that the migration process can be rolled back if an error occurs.
+
+Refer to the [Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) documentation to learn more about workflows.
+
+### Workflow Steps
+
+The workflow you'll create will have the following steps:
+
+- [getMagentoProductsStep](#getMagentoProductsStep): Retrieve products from Magento using the Magento Module.
+- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve Medusa store details, which you'll need when creating the products.
+- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve a shipping profile, which you'll associate the created products with.
+- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve Magento products that are already in Medusa to update them, instead of creating them.
+- [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/index.html.md): Create products in the Medusa application.
+- [updateProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductsWorkflow/index.html.md): Update existing products in the Medusa application.
+
+You only need to implement the `getMagentoProductsStep` step, which retrieves the products from Magento. The other steps and workflows are provided by Medusa's `@medusajs/medusa/core-flows` package.
+
+### getMagentoProductsStep
+
+The first step of the workflow retrieves and returns the products from Magento.
+
+In your plugin, create the file `src/workflows/steps/get-magento-products.ts` with the following content:
+
+
+
+```ts title="src/workflows/steps/get-magento-products.ts"
+import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
+import { MAGENTO_MODULE } from "../../modules/magento"
+import MagentoModuleService from "../../modules/magento/service"
+
+type GetMagentoProductsInput = {
+ currentPage: number
+ pageSize: number
+}
+
+export const getMagentoProductsStep = createStep(
+ "get-magento-products",
+ async ({ currentPage, pageSize }: GetMagentoProductsInput, { container }) => {
+ const magentoModuleService: MagentoModuleService =
+ container.resolve(MAGENTO_MODULE)
+
+ const response = await magentoModuleService.getProducts({
+ currentPage,
+ pageSize,
+ })
+
+ return new StepResponse(response)
+ }
+)
+```
+
+You create a step using `createStep` from the Workflows SDK. It accepts two parameters:
+
+1. The step's name, which is `get-magento-products`.
+2. An async function that executes the step's logic. The function receives two parameters:
+ - The input data for the step, which in this case is the pagination parameters.
+ - An object holding the workflow's context, including the [Medusa Container](https://docs.medusajs.com/docslearn/fundamentals/medusa-container/index.html.md) that allows you to resolve Framework and commerce tools.
+
+In the step function, you resolve the Magento Module's service from the container, then use its `getProducts` method to retrieve the products from Magento.
+
+Steps that return data must return them in a `StepResponse` instance. The `StepResponse` constructor accepts as a parameter the data to return.
+
+### Create migrateProductsFromMagentoWorkflow
+
+You'll now create the workflow that migrates products from Magento using the step you created and steps from Medusa's `@medusajs/medusa/core-flows` package.
+
+In your plugin, create the file `src/workflows/migrate-products-from-magento.ts` with the following content:
+
+
+
+```ts title="src/workflows/migrate-products-from-magento.ts"
+import {
+ createWorkflow, transform, WorkflowResponse,
+} from "@medusajs/framework/workflows-sdk"
+import {
+ CreateProductWorkflowInputDTO, UpsertProductDTO,
+} from "@medusajs/framework/types"
+import {
+ createProductsWorkflow,
+ updateProductsWorkflow,
+ useQueryGraphStep,
+} from "@medusajs/medusa/core-flows"
+import { getMagentoProductsStep } from "./steps/get-magento-products"
+
+type MigrateProductsFromMagentoWorkflowInput = {
+ currentPage: number
+ pageSize: number
+}
+
+export const migrateProductsFromMagentoWorkflowId =
+ "migrate-products-from-magento"
+
+export const migrateProductsFromMagentoWorkflow = createWorkflow(
+ {
+ name: migrateProductsFromMagentoWorkflowId,
+ retentionTime: 10000,
+ store: true,
+ },
+ (input: MigrateProductsFromMagentoWorkflowInput) => {
+ const { pagination, products, attributes } = getMagentoProductsStep(
+ input
+ )
+ // TODO prepare data to create and update products
+ }
+)
+```
+
+You create a workflow using `createWorkflow` from the Workflows SDK. It accepts two parameters:
+
+1. An object with the workflow's configuration, including the name and whether to store the workflow's executions. You enable storing the workflow execution so that you can view it later in the Medusa Admin dashboard.
+2. A worflow constructor function, which holds the workflow's implementation. The function receives the input data for the workflow, which is the pagination parameters.
+
+In the workflow constructor function, you use the `getMagentoProductsStep` step to retrieve the products from Magento, passing it the pagination parameters from the workflow's input.
+
+Next, you'll retrieve the Medusa store details and shipping profiles. These are necessary to prepare the data of the products to create or update.
+
+Replace the `TODO` in the workflow function with the following:
+
+```ts title="src/workflows/migrate-products-from-magento.ts"
+const { data: stores } = useQueryGraphStep({
+ entity: "store",
+ fields: ["supported_currencies.*", "default_sales_channel_id"],
+ pagination: {
+ take: 1,
+ skip: 0,
+ },
+})
+
+const { data: shippingProfiles } = useQueryGraphStep({
+ entity: "shipping_profile",
+ fields: ["id"],
+ pagination: {
+ take: 1,
+ skip: 0,
+ },
+}).config({ name: "get-shipping-profiles" })
+
+// TODO retrieve existing products
+```
+
+You use the `useQueryGraphStep` step to retrieve the store details and shipping profiles. `useQueryGraphStep` is a Medusa step that wraps [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), allowing you to use it in a workflow. Query is a tool that retrieves data across modules.
+
+Whe retrieving the store details, you specifically retrieve its supported currencies and default sales channel ID. You'll associate the products with the store's default sales channel, and set their variant prices in the supported currencies. You'll also associate the products with a shipping profile.
+
+Next, you'll retrieve products that were previously migrated from Magento to determine which products to create or update. Replace the `TODO` with the following:
+
+```ts title="src/workflows/migrate-products-from-magento.ts"
+const externalIdFilters = transform({
+ products,
+}, (data) => {
+ return data.products.map((product) => product.id.toString())
+})
+
+const { data: existingProducts } = useQueryGraphStep({
+ entity: "product",
+ fields: ["id", "external_id", "variants.id", "variants.metadata"],
+ filters: {
+ external_id: externalIdFilters,
+ },
+}).config({ name: "get-existing-products" })
+
+// TODO prepare products to create or update
+```
+
+Since the Medusa application creates an internal representation of the workflow's constructor function, you can't manipulate data directly, as variables have no value while creating the internal representation.
+
+Refer to the [Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md) documentation to learn more about the workflow constructor function's constraints.
+
+Instead, you can manipulate data in a workflow's constructor function using `transform` from the Workflows SDK. `transform` is a function that accepts two parameters:
+
+- The data to transform, which in this case is the Magento products.
+- A function that transforms the data. The function receives the data passed in the first parameter and returns the transformed data.
+
+In the transformation function, you return the IDs of the Magento products. Then, you use the `useQueryGraphStep` to retrieve products in the Medusa application that have an `external_id` property matching the IDs of the Magento products. You'll use this property to store the IDs of the products in Magento.
+
+Next, you'll prepare the data to create and update the products. Replace the `TODO` in the workflow function with the following:
+
+```ts title="src/workflows/migrate-products-from-magento.ts" highlights={prepareHighlights}
+const {
+ productsToCreate,
+ productsToUpdate,
+} = transform({
+ products,
+ attributes,
+ stores,
+ shippingProfiles,
+ existingProducts,
+}, (data) => {
+ const productsToCreate = new Map()
+ const productsToUpdate = new Map()
+
+ data.products.forEach((magentoProduct) => {
+ const productData: CreateProductWorkflowInputDTO | UpsertProductDTO = {
+ title: magentoProduct.name,
+ description: magentoProduct.custom_attributes.find(
+ (attr) => attr.attribute_code === "description"
+ )?.value,
+ status: magentoProduct.status === 1 ? "published" : "draft",
+ handle: magentoProduct.custom_attributes.find(
+ (attr) => attr.attribute_code === "url_key"
+ )?.value,
+ external_id: magentoProduct.id.toString(),
+ thumbnail: magentoProduct.media_gallery_entries.find(
+ (entry) => entry.types.includes("thumbnail")
+ )?.file,
+ sales_channels: [{
+ id: data.stores[0].default_sales_channel_id,
+ }],
+ shipping_profile_id: data.shippingProfiles[0].id,
+ }
+ const existingProduct = data.existingProducts.find((p) => p.external_id === productData.external_id)
+
+ if (existingProduct) {
+ productData.id = existingProduct.id
+ }
+
+ productData.options = magentoProduct.extension_attributes.configurable_product_options?.map((option) => {
+ const attribute = data.attributes.find((attr) => attr.attribute_id === parseInt(option.attribute_id))
+ return {
+ title: option.label,
+ values: attribute?.options.filter((opt) => {
+ return option.values.find((v) => v.value_index === parseInt(opt.value))
+ }).map((opt) => opt.label) || [],
+ }
+ }) || []
+
+ productData.variants = magentoProduct.children?.map((child) => {
+ const childOptions: Record = {}
+
+ child.custom_attributes.forEach((attr) => {
+ const attrData = data.attributes.find((a) => a.attribute_code === attr.attribute_code)
+ if (!attrData) {
+ return
+ }
+
+ childOptions[attrData.default_frontend_label] = attrData.options.find((opt) => opt.value === attr.value)?.label || ""
+ })
+
+ const variantExternalId = child.id.toString()
+ const existingVariant = existingProduct.variants.find((v) => v.metadata.external_id === variantExternalId)
+
+ return {
+ title: child.name,
+ sku: child.sku,
+ options: childOptions,
+ prices: data.stores[0].supported_currencies.map(({ currency_code }) => {
+ return {
+ amount: child.price,
+ currency_code,
+ }
+ }),
+ metadata: {
+ external_id: variantExternalId,
+ },
+ id: existingVariant?.id,
+ }
+ })
+
+ productData.images = magentoProduct.media_gallery_entries.filter((entry) => !entry.types.includes("thumbnail")).map((entry) => {
+ return {
+ url: entry.file,
+ metadata: {
+ external_id: entry.id.toString(),
+ },
+ }
+ })
+
+ if (productData.id) {
+ productsToUpdate.set(existingProduct.id, productData)
+ } else {
+ productsToCreate.set(productData.external_id!, productData)
+ }
+ })
+
+ return {
+ productsToCreate: Array.from(productsToCreate.values()),
+ productsToUpdate: Array.from(productsToUpdate.values()),
+ }
+})
+
+// TODO create and update products
+```
+
+You use `transform` again to prepare the data to create and update the products in the Medusa application. For each Magento product, you map its equivalent Medusa product's data:
+
+- You set the product's general details, such as the title, description, status, handle, external ID, and thumbnail using the Magento product's data and custom attributes.
+- You associate the product with the default sales channel and shipping profile retrieved previously.
+- You map the Magento product's configurable product options to Medusa product options. In Medusa, a product's option has a label, such as "Color", and values, such as "Red". To map the option values, you use the attributes retrieved from Magento.
+- You map the Magento product's children to Medusa product variants. For the variant options, you pass an object whose keys is the option's label, such as "Color", and values is the option's value, such as "Red". For the prices, you set the variant's price based on the Magento child's price for every supported currency in the Medusa store. Also, you set the Magento child product's ID in the Medusa variant's `metadata.external_id` property.
+- You map the Magento product's media gallery entries to Medusa product images. You filter out the thumbnail image and set the URL and the Magento image's ID in the Medusa image's `metadata.external_id` property.
+
+In addition, you use the existing products retrieved in the previous step to determine whether a product should be created or updated. If there's an existing product whose `external_id` matches the ID of the magento product, you set the existing product's ID in the `id` property of the product to be updated. You also do the same for its variants.
+
+Finally, you return the products to create and update.
+
+The last steps of the workflow is to create and update the products. Replace the `TODO` in the workflow function with the following:
+
+```ts title="src/workflows/migrate-products-from-magento.ts"
+createProductsWorkflow.runAsStep({
+ input: {
+ products: productsToCreate,
+ },
+})
+
+updateProductsWorkflow.runAsStep({
+ input: {
+ products: productsToUpdate,
+ },
+})
+
+return new WorkflowResponse(pagination)
+```
+
+You use the `createProductsWorkflow` and `updateProductsWorkflow` workflows from Medusa's `@medusajs/medusa/core-flows` package to create and update the products in the Medusa application.
+
+Workflows must return an instance of `WorkflowResponse`, passing as a parameter the data to return to the workflow's executor. This workflow returns the pagination parameters, allowing you to paginate the product migration process.
+
+You can now use this workflow to migrate products from Magento to Medusa. You'll learn how to use it in the next steps.
+
+***
+
+## Step 6: Schedule Product Migration
+
+There are many ways to execute tasks asynchronously in Medusa, such as [scheduling a job](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md) or [handling emitted events](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md).
+
+In this guide, you'll learn how to schedule the product migration at a specified interval using a scheduled job. A scheduled job is an asynchronous function that the Medusa application runs at the interval you specify during the Medusa application's runtime.
+
+Refer to the [Scheduled Jobs](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md) documentation to learn more about scheduled jobs.
+
+To create a scheduled job, in your plugin, create the file `src/jobs/migrate-magento.ts` with the following content:
+
+
+
+```ts title="src/jobs/migrate-magento.ts"
+import { MedusaContainer } from "@medusajs/framework/types"
+import { migrateProductsFromMagentoWorkflow } from "../workflows"
+
+export default async function migrateMagentoJob(
+ container: MedusaContainer
+) {
+ const logger = container.resolve("logger")
+ logger.info("Migrating products from Magento...")
+
+ let currentPage = 0
+ const pageSize = 100
+ let totalCount = 0
+
+ do {
+ currentPage++
+
+ const {
+ result: pagination,
+ } = await migrateProductsFromMagentoWorkflow(container).run({
+ input: {
+ currentPage,
+ pageSize,
+ },
+ })
+
+ totalCount = pagination.total_count
+ } while (currentPage * pageSize < totalCount)
+
+ logger.info("Finished migrating products from Magento")
+}
+
+export const config = {
+ name: "migrate-magento-job",
+ schedule: "0 0 * * *",
+}
+```
+
+A scheduled job file must export:
+
+- An asynchronous function that executes the job's logic. The function receives the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) as a parameter.
+- An object with the job's configuration, including the name and the schedule. The schedule is a cron job pattern as a string.
+
+In the job function, you resolve the [logger](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md) from the container to log messages. Then, you paginate the product migration process by running the `migrateProductsFromMagentoWorkflow` workflow at each page until you've migrated all products. You use the pagination result returned by the workflow to determine whether there are more products to migrate.
+
+Based on the job's configurations, the Medusa application will run the job at midnight every day.
+
+### Test it Out
+
+To test out this scheduled job, first, change the configuration to run the job every minute:
+
+```ts title="src/jobs/migrate-magento.ts"
+export const config = {
+ // ...
+ schedule: "* * * * *",
+}
+```
+
+Then, make sure to run the `plugin:develop` command in the plugin if you haven't already:
+
+```bash
+npx medusa plugin:develop
+```
+
+This ensures that the plugin's latest changes are reflected in the Medusa application.
+
+Finally, start the Medusa application that the plugin is installed in:
+
+```bash npm2yarn
+npm run dev
+```
+
+After a minute, you'll see a message in the terminal indicating that the migration started:
+
+```plain title="Terminal"
+info: Migrating products from Magento...
+```
+
+Once the migration is done, you'll see the following message:
+
+```plain title="Terminal"
+info: Finished migrating products from Magento
+```
+
+To confirm that the products were migrated, open the Medusa Admin dashboard at `http://localhost:9000/app` and log in. Then, click on Products in the sidebar. You'll see your magento products in the list of products.
+
+
+
+***
+
+## Next Steps
+
+You've now implemented the logic to migrate products from Magento to Medusa. You can re-use the plugin across Medusa applications. You can also expand on the plugin to:
+
+- Migrate other entities, such as orders, customers, and categories. Migrating other entities follows the same pattern as migrating products, using workflows and scheduled jobs. You only need to format the data to be migrated as needed.
+- Allow triggering migrations from the Medusa Admin dashboard using [Admin Customizations](https://docs.medusajs.com/docs/learn/fundamentals/admin/index.html.md). This feature is available in the [Example Repository](https://github.com/medusajs/example-repository/tree/main/src/admin).
+
+If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth learning of all the concepts you've used in this guide and more.
+
+To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).
+
+
# Integrate Medusa with Sanity (CMS)
In this guide, you'll learn how to integrate Medusa with Sanity.
@@ -59712,115 +59870,114 @@ To learn more about the commerce features that Medusa provides, check out Medusa
## JS SDK Admin
+- [batchSalesChannels](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.batchSalesChannels/index.html.md)
+- [delete](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.delete/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)
+- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.retrieve/index.html.md)
+- [update](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.update/index.html.md)
+- [revoke](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.revoke/index.html.md)
- [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)
-- [update](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.update/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.delete/index.html.md)
-- [list](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.list/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.retrieve/index.html.md)
-- [batchSalesChannels](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.batchSalesChannels/index.html.md)
-- [create](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.create/index.html.md)
-- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.retrieve/index.html.md)
-- [delete](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.delete/index.html.md)
-- [list](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.list/index.html.md)
-- [revoke](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.revoke/index.html.md)
-- [update](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.update/index.html.md)
-- [list](https://docs.medusajs.com/references/js_sdk/admin/Currency/methods/js_sdk.admin.Currency.list/index.html.md)
+- [list](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.list/index.html.md)
+- [update](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.update/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Currency/methods/js_sdk.admin.Currency.retrieve/index.html.md)
-- [removeItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.removeItem/index.html.md)
-- [setItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.setItem/index.html.md)
-- [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)
-- [getItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.getItem/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)
-- [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)
-- [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)
+- [list](https://docs.medusajs.com/references/js_sdk/admin/Currency/methods/js_sdk.admin.Currency.list/index.html.md)
+- [fetchStream](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.fetchStream/index.html.md)
+- [fetch](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.fetch/index.html.md)
- [getApiKeyHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getApiKeyHeader_/index.html.md)
+- [clearToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.clearToken_/index.html.md)
+- [getJwtHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getJwtHeader_/index.html.md)
+- [getPublishableKeyHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getPublishableKeyHeader_/index.html.md)
+- [getTokenStorageInfo\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getTokenStorageInfo_/index.html.md)
- [initClient](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.initClient/index.html.md)
-- [throwError\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.throwError_/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)
-- [addInboundItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addInboundItems/index.html.md)
-- [addInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addInboundShipping/index.html.md)
-- [addItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addItems/index.html.md)
-- [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)
-- [addOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addOutboundShipping/index.html.md)
-- [create](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.create/index.html.md)
-- [deleteInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.deleteInboundShipping/index.html.md)
-- [addOutboundItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addOutboundItems/index.html.md)
-- [deleteOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.deleteOutboundShipping/index.html.md)
-- [removeItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeItem/index.html.md)
-- [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)
-- [removeOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeOutboundItem/index.html.md)
-- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.retrieve/index.html.md)
-- [updateInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateInboundShipping/index.html.md)
-- [request](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.request/index.html.md)
-- [updateItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateItem/index.html.md)
-- [updateOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateOutboundItem/index.html.md)
-- [updateOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateOutboundShipping/index.html.md)
-- [updateInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateInboundItem/index.html.md)
-- [create](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.create/index.html.md)
-- [batchCustomers](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.batchCustomers/index.html.md)
-- [delete](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.delete/index.html.md)
-- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.retrieve/index.html.md)
-- [update](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.update/index.html.md)
-- [list](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.list/index.html.md)
+- [getToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getToken_/index.html.md)
+- [throwError\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.throwError_/index.html.md)
+- [getItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.getItem/index.html.md)
+- [removeItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.removeItem/index.html.md)
- [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)
+- [createAddress](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.createAddress/index.html.md)
+- [setItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.setItem/index.html.md)
+- [delete](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.delete/index.html.md)
- [deleteAddress](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.deleteAddress/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.list/index.html.md)
-- [delete](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.delete/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.retrieve/index.html.md)
- [retrieveAddress](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.retrieveAddress/index.html.md)
-- [listAddresses](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.listAddresses/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)
+- [listAddresses](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.listAddresses/index.html.md)
+- [batchCustomers](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.batchCustomers/index.html.md)
+- [create](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.create/index.html.md)
+- [delete](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.delete/index.html.md)
+- [list](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.list/index.html.md)
+- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.retrieve/index.html.md)
+- [update](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.update/index.html.md)
+- [addItems](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.addItems/index.html.md)
- [addPromotions](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.addPromotions/index.html.md)
- [addShippingMethod](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.addShippingMethod/index.html.md)
-- [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)
-- [addItems](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.addItems/index.html.md)
-- [convertToOrder](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.convertToOrder/index.html.md)
+- [beginEdit](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.beginEdit/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)
+- [convertToOrder](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.convertToOrder/index.html.md)
+- [removeActionItem](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.removeActionItem/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)
-- [removePromotions](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.removePromotions/index.html.md)
-- [requestEdit](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.requestEdit/index.html.md)
- [removeShippingMethod](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.removeShippingMethod/index.html.md)
-- [removeActionItem](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.removeActionItem/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)
-- [list](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.list/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)
-- [updateActionItem](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.updateActionItem/index.html.md)
+- [removePromotions](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.removePromotions/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.update/index.html.md)
+- [updateActionShippingMethod](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.updateActionShippingMethod/index.html.md)
+- [updateActionItem](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.updateActionItem/index.html.md)
+- [updateItem](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.updateItem/index.html.md)
- [updateShippingMethod](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.updateShippingMethod/index.html.md)
-- [create](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.create/index.html.md)
-- [createShipment](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.createShipment/index.html.md)
-- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.cancel/index.html.md)
+- [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)
+- [addOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addOutboundShipping/index.html.md)
+- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.cancel/index.html.md)
+- [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)
+- [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)
+- [list](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.list/index.html.md)
+- [deleteOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.deleteOutboundShipping/index.html.md)
+- [removeInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.removeInboundItem/index.html.md)
+- [request](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.request/index.html.md)
+- [removeOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.removeOutboundItem/index.html.md)
+- [updateInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateInboundShipping/index.html.md)
+- [updateInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateInboundItem/index.html.md)
+- [updateOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateOutboundShipping/index.html.md)
+- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.retrieve/index.html.md)
+- [updateOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateOutboundItem/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentProvider/methods/js_sdk.admin.FulfillmentProvider.list/index.html.md)
- [listFulfillmentOptions](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentProvider/methods/js_sdk.admin.FulfillmentProvider.listFulfillmentOptions/index.html.md)
-- [delete](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.delete/index.html.md)
-- [createServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.createServiceZone/index.html.md)
-- [updateServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.updateServiceZone/index.html.md)
-- [retrieveServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.retrieveServiceZone/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)
+- [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)
+- [createServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.createServiceZone/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)
+- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Notification/methods/js_sdk.admin.Notification.retrieve/index.html.md)
- [batchInventoryItemLocationLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.batchInventoryItemLocationLevels/index.html.md)
- [batchInventoryItemsLocationLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.batchInventoryItemsLocationLevels/index.html.md)
- [batchUpdateLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.batchUpdateLevels/index.html.md)
-- [delete](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.delete/index.html.md)
-- [list](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.list/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.create/index.html.md)
-- [listLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.listLevels/index.html.md)
+- [list](https://docs.medusajs.com/references/js_sdk/admin/Notification/methods/js_sdk.admin.Notification.list/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)
-- [update](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.update/index.html.md)
+- [list](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.list/index.html.md)
+- [listLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.listLevels/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.retrieve/index.html.md)
+- [update](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.update/index.html.md)
- [updateLevel](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.updateLevel/index.html.md)
- [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)
@@ -59828,211 +59985,212 @@ To learn more about the commerce features that Medusa provides, check out Medusa
- [list](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.list/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.retrieve/index.html.md)
- [resend](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.resend/index.html.md)
+- [capture](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.capture/index.html.md)
+- [list](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.list/index.html.md)
+- [listPaymentProviders](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.listPaymentProviders/index.html.md)
+- [refund](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.refund/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)
+- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.retrieve/index.html.md)
- [cancelTransfer](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancelTransfer/index.html.md)
+- [cancelFulfillment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancelFulfillment/index.html.md)
- [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)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.list/index.html.md)
+- [createShipment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.createShipment/index.html.md)
- [listChanges](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.listChanges/index.html.md)
-- [markAsDelivered](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.markAsDelivered/index.html.md)
- [listLineItems](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.listLineItems/index.html.md)
- [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)
+- [markAsDelivered](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.markAsDelivered/index.html.md)
- [requestTransfer](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.requestTransfer/index.html.md)
- [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/Notification/methods/js_sdk.admin.Notification.list/index.html.md)
-- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Notification/methods/js_sdk.admin.Notification.retrieve/index.html.md)
-- [addInboundItems](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addInboundItems/index.html.md)
-- [addInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addInboundShipping/index.html.md)
-- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.cancel/index.html.md)
-- [addOutboundItems](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addOutboundItems/index.html.md)
-- [addOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addOutboundShipping/index.html.md)
-- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.cancelRequest/index.html.md)
-- [create](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.create/index.html.md)
-- [deleteInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.deleteInboundShipping/index.html.md)
-- [list](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.list/index.html.md)
-- [removeInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.removeInboundItem/index.html.md)
-- [request](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.request/index.html.md)
-- [removeOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.removeOutboundItem/index.html.md)
-- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.retrieve/index.html.md)
-- [updateInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateInboundItem/index.html.md)
-- [updateInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateInboundShipping/index.html.md)
-- [updateOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateOutboundItem/index.html.md)
-- [updateOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateOutboundShipping/index.html.md)
-- [deleteOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.deleteOutboundShipping/index.html.md)
-- [capture](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.capture/index.html.md)
-- [list](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.list/index.html.md)
-- [refund](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.refund/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)
-- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.retrieve/index.html.md)
-- [listPaymentProviders](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.listPaymentProviders/index.html.md)
+- [addItems](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.addItems/index.html.md)
- [confirm](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.confirm/index.html.md)
- [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)
+- [initiateRequest](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.initiateRequest/index.html.md)
- [updateAddedItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.updateAddedItem/index.html.md)
- [updateOriginalItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.updateOriginalItem/index.html.md)
-- [initiateRequest](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.initiateRequest/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)
+- [addInboundItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addInboundItems/index.html.md)
+- [addInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addInboundShipping/index.html.md)
+- [addItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addItems/index.html.md)
+- [addOutboundItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addOutboundItems/index.html.md)
+- [addOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addOutboundShipping/index.html.md)
+- [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)
+- [deleteOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.deleteOutboundShipping/index.html.md)
+- [deleteInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.deleteInboundShipping/index.html.md)
+- [list](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.list/index.html.md)
+- [create](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.create/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)
+- [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)
+- [updateInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateInboundItem/index.html.md)
+- [updateInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateInboundShipping/index.html.md)
+- [updateItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateItem/index.html.md)
+- [updateOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateOutboundItem/index.html.md)
+- [updateOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateOutboundShipping/index.html.md)
- [markAsPaid](https://docs.medusajs.com/references/js_sdk/admin/PaymentCollection/methods/js_sdk.admin.PaymentCollection.markAsPaid/index.html.md)
+- [delete](https://docs.medusajs.com/references/js_sdk/admin/PaymentCollection/methods/js_sdk.admin.PaymentCollection.delete/index.html.md)
+- [create](https://docs.medusajs.com/references/js_sdk/admin/PaymentCollection/methods/js_sdk.admin.PaymentCollection.create/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Plugin/methods/js_sdk.admin.Plugin.list/index.html.md)
-- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.create/index.html.md)
-- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.delete/index.html.md)
-- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.retrieve/index.html.md)
-- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.list/index.html.md)
-- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.update/index.html.md)
-- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.create/index.html.md)
-- [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)
-- [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/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)
-- [create](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.create/index.html.md)
-- [delete](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.delete/index.html.md)
-- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.retrieve/index.html.md)
-- [update](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.update/index.html.md)
-- [list](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.list/index.html.md)
-- [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.updateProducts/index.html.md)
- [batch](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batch/index.html.md)
-- [confirmImport](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.confirmImport/index.html.md)
-- [batchVariants](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batchVariants/index.html.md)
- [batchVariantInventoryItems](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batchVariantInventoryItems/index.html.md)
+- [batchVariants](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batchVariants/index.html.md)
+- [confirmImport](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.confirmImport/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.create/index.html.md)
-- [createVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createVariant/index.html.md)
- [createImport](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createImport/index.html.md)
-- [delete](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.delete/index.html.md)
- [createOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createOption/index.html.md)
-- [export](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.export/index.html.md)
+- [createVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createVariant/index.html.md)
- [deleteOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.deleteOption/index.html.md)
+- [delete](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.delete/index.html.md)
- [deleteVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.deleteVariant/index.html.md)
- [import](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.import/index.html.md)
-- [listOptions](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.listOptions/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.list/index.html.md)
+- [export](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.export/index.html.md)
+- [listOptions](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.listOptions/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieve/index.html.md)
- [retrieveOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieveOption/index.html.md)
-- [retrieveVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieveVariant/index.html.md)
-- [update](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.update/index.html.md)
- [listVariants](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.listVariants/index.html.md)
+- [retrieveVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieveVariant/index.html.md)
- [updateVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.updateVariant/index.html.md)
- [updateOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.updateOption/index.html.md)
-- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.create/index.html.md)
-- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.list/index.html.md)
-- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.retrieve/index.html.md)
-- [delete](https://docs.medusajs.com/references/js_sdk/admin/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)
-- [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/PriceList/methods/js_sdk.admin.PriceList.create/index.html.md)
+- [update](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.update/index.html.md)
- [batchPrices](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.batchPrices/index.html.md)
+- [create](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.delete/index.html.md)
- [linkProducts](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.linkProducts/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.list/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.retrieve/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.update/index.html.md)
-- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.delete/index.html.md)
-- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.retrieve/index.html.md)
+- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.delete/index.html.md)
+- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.create/index.html.md)
+- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.retrieve/index.html.md)
+- [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.updateProducts/index.html.md)
+- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.list/index.html.md)
+- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.update/index.html.md)
+- [create](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.create/index.html.md)
+- [delete](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.delete/index.html.md)
+- [list](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.list/index.html.md)
+- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.retrieve/index.html.md)
+- [update](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.update/index.html.md)
+- [create](https://docs.medusajs.com/references/js_sdk/admin/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)
+- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.create/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)
+- [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)
+- [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/ProductTag/methods/js_sdk.admin.ProductTag.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)
+- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.delete/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.list/index.html.md)
-- [list](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.list/index.html.md)
-- [delete](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.delete/index.html.md)
-- [listRuleAttributes](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRuleAttributes/index.html.md)
-- [listRuleValues](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRuleValues/index.html.md)
-- [listRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRules/index.html.md)
-- [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)
-- [removeRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.removeRules/index.html.md)
-- [create](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.create/index.html.md)
-- [list](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.list/index.html.md)
-- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.retrieve/index.html.md)
-- [create](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.create/index.html.md)
-- [update](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.update/index.html.md)
-- [delete](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.delete/index.html.md)
+- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.retrieve/index.html.md)
+- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.update/index.html.md)
- [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)
+- [list](https://docs.medusajs.com/references/js_sdk/admin/RefundReason/methods/js_sdk.admin.RefundReason.list/index.html.md)
+- [create](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.create/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.list/index.html.md)
+- [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)
+- [addRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.addRules/index.html.md)
+- [create](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.create/index.html.md)
+- [delete](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.delete/index.html.md)
+- [list](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.list/index.html.md)
+- [listRuleAttributes](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRuleAttributes/index.html.md)
+- [listRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRules/index.html.md)
+- [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)
+- [listRuleValues](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRuleValues/index.html.md)
+- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductVariant/methods/js_sdk.admin.ProductVariant.list/index.html.md)
+- [update](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.update/index.html.md)
+- [updateRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.updateRules/index.html.md)
+- [create](https://docs.medusajs.com/references/js_sdk/admin/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)
+- [update](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.update/index.html.md)
+- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.retrieve/index.html.md)
+- [addReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.addReturnItem/index.html.md)
+- [addReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.addReturnShipping/index.html.md)
+- [cancelReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancelReceive/index.html.md)
+- [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)
+- [confirmRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.confirmRequest/index.html.md)
+- [deleteReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.deleteReturnShipping/index.html.md)
+- [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)
+- [initiateReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.initiateReceive/index.html.md)
+- [initiateRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.initiateRequest/index.html.md)
+- [list](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.list/index.html.md)
+- [receiveItems](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.receiveItems/index.html.md)
+- [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)
+- [removeDismissItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeDismissItem/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)
+- [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)
+- [updateReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReturnItem/index.html.md)
+- [create](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.create/index.html.md)
+- [updateReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReturnShipping/index.html.md)
+- [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)
+- [update](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.update/index.html.md)
+- [create](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.create/index.html.md)
+- [delete](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.delete/index.html.md)
+- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.retrieve/index.html.md)
+- [list](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.list/index.html.md)
+- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.retrieve/index.html.md)
+- [updateRules](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.updateRules/index.html.md)
+- [update](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.update/index.html.md)
+- [create](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.create/index.html.md)
+- [createFulfillmentSet](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.createFulfillmentSet/index.html.md)
+- [delete](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.delete/index.html.md)
+- [list](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.list/index.html.md)
+- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.retrieve/index.html.md)
+- [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)
+- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Store/methods/js_sdk.admin.Store.retrieve/index.html.md)
+- [update](https://docs.medusajs.com/references/js_sdk/admin/Store/methods/js_sdk.admin.Store.update/index.html.md)
+- [list](https://docs.medusajs.com/references/js_sdk/admin/Store/methods/js_sdk.admin.Store.list/index.html.md)
+- [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)
+- [create](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.create/index.html.md)
+- [delete](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.delete/index.html.md)
+- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.retrieve/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/ShippingProfile/methods/js_sdk.admin.ShippingProfile.update/index.html.md)
+- [list](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.list/index.html.md)
+- [update](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.update/index.html.md)
+- [list](https://docs.medusajs.com/references/js_sdk/admin/TaxProvider/methods/js_sdk.admin.TaxProvider.list/index.html.md)
+- [delete](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.delete/index.html.md)
+- [create](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.create/index.html.md)
+- [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)
- [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)
-- [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/SalesChannel/methods/js_sdk.admin.SalesChannel.list/index.html.md)
-- [update](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.update/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.retrieve/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/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)
-- [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.updateProducts/index.html.md)
-- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.retrieve/index.html.md)
-- [update](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.update/index.html.md)
-- [addReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.addReturnItem/index.html.md)
-- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancel/index.html.md)
-- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancelRequest/index.html.md)
-- [cancelReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancelReceive/index.html.md)
-- [deleteReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.deleteReturnShipping/index.html.md)
-- [confirmReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.confirmReceive/index.html.md)
-- [confirmRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.confirmRequest/index.html.md)
-- [addReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.addReturnShipping/index.html.md)
-- [dismissItems](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.dismissItems/index.html.md)
-- [list](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.list/index.html.md)
-- [initiateReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.initiateReceive/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)
-- [removeReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeReturnItem/index.html.md)
-- [initiateRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.initiateRequest/index.html.md)
-- [removeReceiveItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeReceiveItem/index.html.md)
-- [updateDismissItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateDismissItem/index.html.md)
-- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.retrieve/index.html.md)
-- [updateReceiveItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReceiveItem/index.html.md)
-- [updateReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReturnItem/index.html.md)
-- [updateReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReturnShipping/index.html.md)
-- [updateRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateRequest/index.html.md)
-- [create](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.create/index.html.md)
-- [delete](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.delete/index.html.md)
-- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.retrieve/index.html.md)
-- [update](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.update/index.html.md)
-- [updateRules](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.updateRules/index.html.md)
-- [list](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.list/index.html.md)
-- [delete](https://docs.medusajs.com/references/js_sdk/admin/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)
-- [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)
-- [update](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.update/index.html.md)
-- [create](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.create/index.html.md)
-- [list](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.list/index.html.md)
-- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.retrieve/index.html.md)
-- [delete](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.delete/index.html.md)
-- [update](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.update/index.html.md)
-- [createFulfillmentSet](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.createFulfillmentSet/index.html.md)
-- [updateFulfillmentProviders](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.updateFulfillmentProviders/index.html.md)
-- [updateSalesChannels](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.updateSalesChannels/index.html.md)
-- [delete](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.delete/index.html.md)
-- [create](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.create/index.html.md)
-- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.retrieve/index.html.md)
-- [list](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.list/index.html.md)
-- [update](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.update/index.html.md)
-- [delete](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.delete/index.html.md)
-- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.retrieve/index.html.md)
-- [list](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.list/index.html.md)
-- [update](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.update/index.html.md)
-- [create](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.create/index.html.md)
-- [list](https://docs.medusajs.com/references/js_sdk/admin/TaxProvider/methods/js_sdk.admin.TaxProvider.list/index.html.md)
+- [update](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.update/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.delete/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/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)
- [me](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.me/index.html.md)
+- [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.updateProducts/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)
- [create](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.delete/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.retrieve/index.html.md)
-- [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/User/methods/js_sdk.admin.User.update/index.html.md)
-- [update](https://docs.medusajs.com/references/js_sdk/admin/Store/methods/js_sdk.admin.Store.update/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/WorkflowExecution/methods/js_sdk.admin.WorkflowExecution.list/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/WorkflowExecution/methods/js_sdk.admin.WorkflowExecution.retrieve/index.html.md)
@@ -60041,10 +60199,10 @@ To learn more about the commerce features that Medusa provides, check out Medusa
- [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)
-- [resetPassword](https://docs.medusajs.com/references/js-sdk/auth/resetPassword/index.html.md)
+- [logout](https://docs.medusajs.com/references/js-sdk/auth/logout/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)
-- [logout](https://docs.medusajs.com/references/js-sdk/auth/logout/index.html.md)
+- [resetPassword](https://docs.medusajs.com/references/js-sdk/auth/resetPassword/index.html.md)
- [updateProvider](https://docs.medusajs.com/references/js-sdk/auth/updateProvider/index.html.md)
@@ -60052,11 +60210,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)
- [customer](https://docs.medusajs.com/references/js-sdk/store/customer/index.html.md)
-- [fulfillment](https://docs.medusajs.com/references/js-sdk/store/fulfillment/index.html.md)
-- [product](https://docs.medusajs.com/references/js-sdk/store/product/index.html.md)
+- [collection](https://docs.medusajs.com/references/js-sdk/store/collection/index.html.md)
- [payment](https://docs.medusajs.com/references/js-sdk/store/payment/index.html.md)
+- [product](https://docs.medusajs.com/references/js-sdk/store/product/index.html.md)
+- [fulfillment](https://docs.medusajs.com/references/js-sdk/store/fulfillment/index.html.md)
- [order](https://docs.medusajs.com/references/js-sdk/store/order/index.html.md)
- [region](https://docs.medusajs.com/references/js-sdk/store/region/index.html.md)
@@ -60366,111 +60524,557 @@ export default ProductWidget
This widget also uses a [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) custom component.
-# Header - Admin Components
+# Data Table - Admin Components
-Each section in the Medusa Admin has a header with a title, and optionally a subtitle with buttons to perform an action.
+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.
-To create a component that uses the same header styling and structure, create the file `src/admin/components/header.tsx` with the following content:
+
-```tsx title="src/admin/components/header.tsx"
-import { Heading, Button, Text } from "@medusajs/ui"
-import React from "react"
-import { Link, LinkProps } from "react-router-dom"
-import { ActionMenu, ActionMenuProps } from "./action-menu"
+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.
-export type HeadingProps = {
- title: string
- subtitle?: string
- actions?: (
- {
- type: "button",
- props: React.ComponentProps
- link?: LinkProps
- } |
- {
- type: "action-menu"
- props: ActionMenuProps
- } |
- {
- type: "custom"
- children: React.ReactNode
- }
- )[]
+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 Header = ({
- title,
- subtitle,
- actions = [],
-}: HeadingProps) => {
+export const config = defineRouteConfig({
+ label: "Custom",
+ icon: ChatBubbleLeftRight,
+})
+
+export default CustomPage
+```
+
+
+# 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 `Header` component shows a title, and optionally a subtitle and action buttons.
-
-The component also uses the [Action Menu](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/action-menu/index.html.md) custom component.
+The `SectionRow` component shows a title and a value in the same row.
It accepts the following props:
-- title: (\`string\`) The section's title.
-- subtitle: (\`string\`) The section's subtitle.
-- actions: (\`object\[]\`) An array of actions to show.
-
- - type: (\`button\` \\| \`action-menu\` \\| \`custom\`) The type of action to add.
-
- \- If its value is \`button\`, it'll show a button that can have a link or an on-click action.
-
- \- If its value is \`action-menu\`, it'll show a three dot icon with a dropdown of actions.
-
- \- If its value is \`custom\`, you can pass any React nodes to render.
-
- - props: (object)
-
- - children: (React.ReactNode) This property is only accepted if \`type\` is \`custom\`. Its content is rendered as part of the actions.
+- title: (\`string\`) The title to show on the left side.
+- value: (\`React.ReactNode\` \\| \`string\` \\| \`null\`) The value to show on the right side.
+- actions: (\`React.ReactNode\`) The actions to show at the end of the row.
***
## Example
-Use the `Header` component in any widget or UI route.
+Use the `SectionRow` component in any widget or UI route.
For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content:
@@ -60478,26 +61082,13 @@ For example, create the widget `src/admin/widgets/product-widget.tsx` with the f
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container } from "../components/container"
import { Header } from "../components/header"
+import { SectionRow } from "../components/section-row"
const ProductWidget = () => {
return (
- {
- alert("You clicked the button.")
- },
- },
- },
- ]}
- />
+
+
)
}
@@ -60509,235 +61100,7 @@ export const config = defineWidgetConfig({
export default ProductWidget
```
-This widget also uses a [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) custom component.
-
-
-# JSON View - Admin Components
-
-Detail pages in the Medusa Admin show a JSON section to view the current page's details in JSON format.
-
-
-
-To create a component that shows a JSON section in your customizations, create the file `src/admin/components/json-view-section.tsx` with the following content:
-
-```tsx title="src/admin/components/json-view-section.tsx"
-import {
- ArrowUpRightOnBox,
- Check,
- SquareTwoStack,
- TriangleDownMini,
- XMarkMini,
-} from "@medusajs/icons"
-import {
- Badge,
- Container,
- Drawer,
- Heading,
- IconButton,
- Kbd,
-} from "@medusajs/ui"
-import Primitive from "@uiw/react-json-view"
-import { CSSProperties, MouseEvent, Suspense, useState } from "react"
-
-type JsonViewSectionProps = {
- data: object
- title?: string
-}
-
-export const JsonViewSection = ({ data }: JsonViewSectionProps) => {
- const numberOfKeys = Object.keys(data).length
-
- return (
-
-
-
-
-
-
- )
-}
-
-type CopiedProps = {
- style?: CSSProperties
- value: object | undefined
-}
-
-const Copied = ({ style, value }: CopiedProps) => {
- const [copied, setCopied] = useState(false)
-
- const handler = (e: MouseEvent) => {
- e.stopPropagation()
- setCopied(true)
-
- if (typeof value === "string") {
- navigator.clipboard.writeText(value)
- } else {
- const json = JSON.stringify(value, null, 2)
- navigator.clipboard.writeText(json)
- }
-
- setTimeout(() => {
- setCopied(false)
- }, 2000)
- }
-
- const styl = { whiteSpace: "nowrap", width: "20px" }
-
- if (copied) {
- return (
-
-
-
- )
- }
-
- return (
-
-
-
- )
-}
-```
-
-The `JsonViewSection` component shows a section with the "JSON" title and a button to show the data as JSON in a drawer or side window.
-
-The `JsonViewSection` accepts a `data` prop, which is the data to show as a JSON object in the drawer.
-
-***
-
-## Example
-
-Use the `JsonViewSection` component in any widget or UI route.
-
-For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content:
-
-```tsx title="src/admin/widgets/product-widget.tsx"
-import { defineWidgetConfig } from "@medusajs/admin-sdk"
-import { JsonViewSection } from "../components/json-view-section"
-
-const ProductWidget = () => {
- return
-}
-
-export const config = defineWidgetConfig({
- zone: "product.details.before",
-})
-
-export default ProductWidget
-```
-
-This shows the JSON section at the top of the product page, passing it the object `{ name: "John" }`.
+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.
# Forms - Admin Components
@@ -61313,492 +61676,379 @@ This component uses the [Container](https://docs.medusajs.com/Users/shahednasser
It will add at the top of a product's details page a new section, and in its header you'll find an "Edit Item" button. If you click on it, it will open the drawer with your form.
-# Data Table - Admin Components
+# Header - Admin Components
-This component is available after [Medusa v2.4.0+](https://github.com/medusajs/medusa/releases/tag/v2.4.0).
+Each section in the Medusa Admin has a header with a title, and optionally a subtitle with buttons to perform an action.
-The [DataTable component in Medusa UI](https://docs.medusajs.com/ui/components/data-table/index.html.md) allows you to display data in a table with sorting, filtering, and pagination. It's used across the Medusa Admin dashboard to showcase a list of items, such as a list of products.
+
-
+To create a component that uses the same header styling and structure, create the file `src/admin/components/header.tsx` with the following content:
-You can use this component in your Admin Extensions to display data in a table format, especially if you're retrieving them from API routes of the Medusa application.
+```tsx title="src/admin/components/header.tsx"
+import { Heading, Button, Text } from "@medusajs/ui"
+import React from "react"
+import { Link, LinkProps } from "react-router-dom"
+import { ActionMenu, ActionMenuProps } from "./action-menu"
-This guide focuses on how to use the `DataTable` component while fetching data from the backend. Refer to the [Medusa UI documentation](https://docs.medusajs.com/ui/components/data-table/index.html.md) for detailed information about the DataTable component and its different usages.
+export type HeadingProps = {
+ title: string
+ subtitle?: string
+ actions?: (
+ {
+ type: "button",
+ props: React.ComponentProps
+ link?: LinkProps
+ } |
+ {
+ type: "action-menu"
+ props: ActionMenuProps
+ } |
+ {
+ type: "custom"
+ children: React.ReactNode
+ }
+ )[]
+}
-## 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
+export const Header = ({
+ title,
+ subtitle,
+ actions = [],
+}: HeadingProps) => {
+ return (
+
+ )
}
```
-In the component, you've added the following state variables:
+The `Header` component shows a title, and optionally a subtitle and action buttons.
-- `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.
+The component also uses the [Action Menu](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/action-menu/index.html.md) custom component.
-You've also added two memoized variables:
+It accepts the following props:
-- `offset`: How many items to skip when fetching data based on the current page.
-- `statusFilters`: The selected status filters, if any.
+- title: (\`string\`) The section's title.
+- subtitle: (\`string\`) The section's subtitle.
+- actions: (\`object\[]\`) An array of actions to show.
-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:
+ - type: (\`button\` \\| \`action-menu\` \\| \`custom\`) The type of action to add.
-```tsx title="src/admin/routes/custom/page.tsx"
-import { sdk } from "../../lib/config"
-import { useQuery } from "@tanstack/react-query"
-```
+ \- If its value is \`button\`, it'll show a button that can have a link or an on-click action.
-This imports the JS SDK instance and `useQuery` from [Tanstack Query](https://tanstack.com/query/latest).
+ \- If its value is \`action-menu\`, it'll show a three dot icon with a dropdown of actions.
-Then, replace the `TODO` in the component with the following:
+ \- If its value is \`custom\`, you can pass any React nodes to render.
-```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]],
-})
+ - props: (object)
-// TODO configure data table
-```
+ - children: (React.ReactNode) This property is only accepted if \`type\` is \`custom\`. Its content is rendered as part of the actions.
-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.
+## Example
-So, whenever the user changes the current page, search query, status filters, or sorting, the products are fetched based on the new parameters.
+Use the `Header` component in any widget or UI route.
-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:
+For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content:
-```tsx title="src/admin/routes/custom/page.tsx"
-import {
- // ...
- useDataTable,
-} from "@medusajs/ui"
-import { useNavigate } from "react-router-dom"
-```
-
-Then, replace the `TODO` in the component with the following:
-
-```tsx title="src/admin/routes/custom/page.tsx"
-const navigate = useNavigate()
-
-const table = useDataTable({
- columns,
- data: data?.products || [],
- getRowId: (row) => row.id,
- rowCount: data?.count || 0,
- isLoading,
- pagination: {
- state: pagination,
- onPaginationChange: setPagination,
- },
- search: {
- state: search,
- onSearchChange: setSearch,
- },
- filtering: {
- state: filtering,
- onFilteringChange: setFiltering,
- },
- filters,
- sorting: {
- // Pass the pagination state and updater to the table instance
- state: sorting,
- onSortingChange: setSorting,
- },
- onRowClick: (event, row) => {
- // Handle row click, for example
- navigate(`/products/${row.id}`)
- },
-})
-
-// TODO render component
-```
-
-The `useDataTable` hook accepts an object with the following properties:
-
-- columns: (\`array\`) The columns to display in the data table. You created this using the \`createDataTableColumnHelper\` utility.
-- data: (\`array\`) The products fetched from the Medusa application.
-- getRowId: (\`function\`) A function that returns the unique ID of a row.
-- rowCount: (\`number\`) The total number of products that can be retrieved. This is used to determine the number of pages.
-- isLoading: (\`boolean\`) A boolean that indicates if the data is being fetched.
-- pagination: (\`object\`) An object to configure pagination.
-
- - state: (\`object\`) The pagination React state variable.
-
- - onPaginationChange: (\`function\`) A function that updates the pagination state.
-- search: (\`object\`) An object to configure searching.
-
- - state: (\`string\`) The search query React state variable.
-
- - onSearchChange: (\`function\`) A function that updates the search query state.
-- filtering: (\`object\`) An object to configure filtering.
-
- - state: (\`object\`) The filtering React state variable.
-
- - onFilteringChange: (\`function\`) A function that updates the filtering state.
-- filters: (\`array\`) The filters to display in the data table. You created this using the \`createDataTableFilterHelper\` utility.
-- sorting: (\`object\`) An object to configure sorting.
-
- - state: (\`object\`) The sorting React state variable.
-
- - onSortingChange: (\`function\`) A function that updates the sorting state.
-- onRowClick: (\`function\`) A function that allows you to perform an action when the user clicks on a row. In this example, you navigate to the product's detail page.
-
- - event: (\`mouseevent\`) An instance of the \[MouseClickEvent]\(https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent) object.
-
- - row: (\`object\`) The data of the row that was clicked.
-
-Finally, you'll render the data table. But first, add the following imports at the top of the page:
-
-```tsx title="src/admin/routes/custom/page.tsx"
-import {
- // ...
- DataTable,
-} from "@medusajs/ui"
-import { SingleColumnLayout } from "../../layouts/single-column"
-import { Container } from "../../components/container"
-```
-
-Aside from the `DataTable` component, you also import the [SingleColumnLayout](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/layouts/single-column/index.html.md) and [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) components implemented in other Admin Component guides. These components ensure a style consistent to other pages in the admin dashboard.
-
-Then, replace the `TODO` in the component with the following:
-
-```tsx title="src/admin/routes/custom/page.tsx"
-return (
-
-
-
-
- Products
-
-
-
-
-
-
-
-
-
-
-
-)
-```
-
-You render the `DataTable` component and pass the `table` instance as a prop. In the `DataTable` component, you render a toolbar showing a heading, filter menu, sorting menu, and a search input. You also show pagination after the table.
-
-Lastly, export the component and the UI widget's configuration at the end of the file:
-
-```tsx title="src/admin/routes/custom/page.tsx"
-// other imports...
-import { defineRouteConfig } from "@medusajs/admin-sdk"
-import { ChatBubbleLeftRight } from "@medusajs/icons"
-
-// ...
-
-export const config = defineRouteConfig({
- label: "Custom",
- icon: ChatBubbleLeftRight,
-})
-
-export default CustomPage
-```
-
-If you start your Medusa application and go to `localhost:9000/app/custom`, you'll see the data table showing the list of products with pagination, filtering, searching, and sorting functionalities.
-
-### Full Example Code
-
-```tsx title="src/admin/routes/custom/page.tsx"
-import { defineRouteConfig } from "@medusajs/admin-sdk"
-import { ChatBubbleLeftRight } from "@medusajs/icons"
-import {
- Badge,
- createDataTableColumnHelper,
- createDataTableFilterHelper,
- DataTable,
- DataTableFilteringState,
- DataTablePaginationState,
- DataTableSortingState,
- Heading,
- useDataTable,
-} from "@medusajs/ui"
-import { useQuery } from "@tanstack/react-query"
-import { SingleColumnLayout } from "../../layouts/single-column"
-import { sdk } from "../../lib/config"
-import { useMemo, useState } from "react"
-import { Container } from "../../components/container"
-import { HttpTypes, ProductStatus } from "@medusajs/framework/types"
-
-const columnHelper = createDataTableColumnHelper()
-
-const columns = [
- columnHelper.accessor("title", {
- header: "Title",
- // Enables sorting for the column.
- enableSorting: true,
- // If omitted, the header will be used instead if it's a string,
- // otherwise the accessor key (id) will be used.
- sortLabel: "Title",
- // If omitted the default value will be "A-Z"
- sortAscLabel: "A-Z",
- // If omitted the default value will be "Z-A"
- sortDescLabel: "Z-A",
- }),
- columnHelper.accessor("status", {
- header: "Status",
- cell: ({ getValue }) => {
- const status = getValue()
- return (
-
- {status === "published" ? "Published" : "Draft"}
-
- )
- },
- }),
-]
-
-const filterHelper = createDataTableFilterHelper()
-
-const filters = [
- filterHelper.accessor("status", {
- type: "select",
- label: "Status",
- options: [
- {
- label: "Published",
- value: "published",
- },
- {
- label: "Draft",
- value: "draft",
- },
- ],
- }),
-]
-
-const limit = 15
-
-const CustomPage = () => {
- const [pagination, setPagination] = useState({
- pageSize: limit,
- pageIndex: 0,
- })
- const [search, setSearch] = useState("")
- const [filtering, setFiltering] = useState({})
- const [sorting, setSorting] = useState(null)
-
- const offset = useMemo(() => {
- return pagination.pageIndex * limit
- }, [pagination])
- const statusFilters = useMemo(() => {
- return (filtering.status || []) as ProductStatus
- }, [filtering])
-
- const { data, isLoading } = useQuery({
- queryFn: () => sdk.admin.product.list({
- limit,
- offset,
- q: search,
- status: statusFilters,
- order: sorting ? `${sorting.desc ? "-" : ""}${sorting.id}` : undefined,
- }),
- queryKey: [["products", limit, offset, search, statusFilters, sorting?.id, sorting?.desc]],
- })
-
- const table = useDataTable({
- columns,
- data: data?.products || [],
- getRowId: (row) => row.id,
- rowCount: data?.count || 0,
- isLoading,
- pagination: {
- state: pagination,
- onPaginationChange: setPagination,
- },
- search: {
- state: search,
- onSearchChange: setSearch,
- },
- filtering: {
- state: filtering,
- onFilteringChange: setFiltering,
- },
- filters,
- sorting: {
- // Pass the pagination state and updater to the table instance
- state: sorting,
- onSortingChange: setSorting,
- },
- })
+```tsx title="src/admin/widgets/product-widget.tsx"
+import { defineWidgetConfig } from "@medusajs/admin-sdk"
+import { Container } from "../components/container"
+import { Header } from "../components/header"
+const ProductWidget = () => {
return (
-
-
-
-
- Products
-
-
-
-
-
-
-
-
-
-
-
+
+ {
+ alert("You clicked the button.")
+ },
+ },
+ },
+ ]}
+ />
+
)
}
-export const config = defineRouteConfig({
- label: "Custom",
- icon: ChatBubbleLeftRight,
+export const config = defineWidgetConfig({
+ zone: "product.details.before",
})
-export default CustomPage
+export default ProductWidget
```
+This widget also uses a [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) custom component.
+
+
+# JSON View - Admin Components
+
+Detail pages in the Medusa Admin show a JSON section to view the current page's details in JSON format.
+
+
+
+To create a component that shows a JSON section in your customizations, create the file `src/admin/components/json-view-section.tsx` with the following content:
+
+```tsx title="src/admin/components/json-view-section.tsx"
+import {
+ ArrowUpRightOnBox,
+ Check,
+ SquareTwoStack,
+ TriangleDownMini,
+ XMarkMini,
+} from "@medusajs/icons"
+import {
+ Badge,
+ Container,
+ Drawer,
+ Heading,
+ IconButton,
+ Kbd,
+} from "@medusajs/ui"
+import Primitive from "@uiw/react-json-view"
+import { CSSProperties, MouseEvent, Suspense, useState } from "react"
+
+type JsonViewSectionProps = {
+ data: object
+ title?: string
+}
+
+export const JsonViewSection = ({ data }: JsonViewSectionProps) => {
+ const numberOfKeys = Object.keys(data).length
+
+ return (
+
+
+
+
+
+
+ )
+}
+
+type CopiedProps = {
+ style?: CSSProperties
+ value: object | undefined
+}
+
+const Copied = ({ style, value }: CopiedProps) => {
+ const [copied, setCopied] = useState(false)
+
+ const handler = (e: MouseEvent) => {
+ e.stopPropagation()
+ setCopied(true)
+
+ if (typeof value === "string") {
+ navigator.clipboard.writeText(value)
+ } else {
+ const json = JSON.stringify(value, null, 2)
+ navigator.clipboard.writeText(json)
+ }
+
+ setTimeout(() => {
+ setCopied(false)
+ }, 2000)
+ }
+
+ const styl = { whiteSpace: "nowrap", width: "20px" }
+
+ if (copied) {
+ return (
+
+
+
+ )
+ }
+
+ return (
+
+
+
+ )
+}
+```
+
+The `JsonViewSection` component shows a section with the "JSON" title and a button to show the data as JSON in a drawer or side window.
+
+The `JsonViewSection` accepts a `data` prop, which is the data to show as a JSON object in the drawer.
+
+***
+
+## Example
+
+Use the `JsonViewSection` component in any widget or UI route.
+
+For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content:
+
+```tsx title="src/admin/widgets/product-widget.tsx"
+import { defineWidgetConfig } from "@medusajs/admin-sdk"
+import { JsonViewSection } from "../components/json-view-section"
+
+const ProductWidget = () => {
+ return
+}
+
+export const config = defineWidgetConfig({
+ zone: "product.details.before",
+})
+
+export default ProductWidget
+```
+
+This shows the JSON section at the top of the product page, passing it the object `{ name: "John" }`.
+
# Table - Admin Components
@@ -62090,98 +62340,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.
@@ -62348,6 +62506,46 @@ Some examples of method names:
The reference uses only the operation name to refer to the method.
+# delete Method - Service Factory Reference
+
+This method deletes one or more records.
+
+## Delete One Record
+
+```ts
+await postModuleService.deletePosts("123")
+```
+
+To delete one record, pass its ID as a parameter of the method.
+
+***
+
+## Delete Multiple Records
+
+```ts
+await postModuleService.deletePosts([
+ "123",
+ "321",
+])
+```
+
+To delete multiple records, pass an array of IDs as a parameter of the method.
+
+***
+
+## Delete Records Matching Filters
+
+```ts
+await postModuleService.deletePosts({
+ name: "My Post",
+})
+```
+
+To delete records matching a set of filters, pass an object of filters as a parameter.
+
+Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md).
+
+
# create Method - Service Factory Reference
This method creates one or more records of the data model.
@@ -62386,124 +62584,6 @@ const posts = await postModuleService.createPosts([
If an array is passed of the method, an array of the created records is also returned.
-# list Method - Service Factory Reference
-
-This method retrieves a list of records.
-
-## Retrieve List of Records
-
-```ts
-const posts = await postModuleService.listPosts()
-```
-
-If no parameters are passed, the method returns an array of the first `15` records.
-
-***
-
-## Filter Records
-
-```ts
-const posts = await postModuleService.listPosts({
- id: ["123", "321"],
-})
-```
-
-### Parameters
-
-To retrieve records matching a set of filters, pass an object of the filters as a first parameter.
-
-Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md).
-
-### Returns
-
-The method returns an array of the first `15` records matching the filters.
-
-***
-
-## Retrieve Relations
-
-This applies to relations between data models of the same module. To retrieve linked records of different modules, use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md).
-
-```ts
-const posts = await postModuleService.listPosts({}, {
- relations: ["author"],
-})
-```
-
-### Parameters
-
-To retrieve records with their relations, pass as a second parameter an object having a `relations` property. `relations`'s value is an array of relation names.
-
-### Returns
-
-The method returns an array of the first `15` records matching the filters.
-
-***
-
-## Select Properties
-
-```ts
-const posts = await postModuleService.listPosts({}, {
- select: ["id", "name"],
-})
-```
-
-### Parameters
-
-By default, retrieved records have all their properties. To select specific properties to retrieve, pass in the second object parameter a `select` property.
-
-`select`'s value is an array of property names to retrieve.
-
-### Returns
-
-The method returns an array of the first `15` records matching the filters.
-
-***
-
-## Paginate Relations
-
-```ts
-const posts = await postModuleService.listPosts({}, {
- take: 20,
- skip: 10,
-})
-```
-
-### Parameters
-
-To paginate the returned records, the second object parameter accepts the following properties:
-
-- `take`: a number indicating how many records to retrieve. By default, it's `15`.
-- `skip`: a number indicating how many records to skip before the retrieved records. By default, it's `0`.
-
-### Returns
-
-The method returns an array of records. The number of records is less than or equal to `take`'s value.
-
-***
-
-## Sort Records
-
-```ts
-const posts = await postModuleService.listPosts({}, {
- order: {
- name: "ASC",
- },
-})
-```
-
-### Parameters
-
-To sort records by one or more properties, pass to the second object parameter the `order` property. Its value is an object whose keys are the property names, and values can either be:
-
-- `ASC` to sort by this property in the ascending order.
-- `DESC` to sort by this property in the descending order.
-
-### Returns
-
-The method returns an array of the first `15` records matching the filters.
-
-
# 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).
@@ -62591,6 +62671,63 @@ restoredPosts = {
```
+# retrieve Method - Service Factory Reference
+
+This method retrieves one record of the data model by its ID.
+
+## Retrieve a Record
+
+```ts
+const post = await postModuleService.retrievePost("123")
+```
+
+### Parameters
+
+Pass the ID of the record to retrieve.
+
+### Returns
+
+The method returns the record as an object.
+
+***
+
+## Retrieve a Record's Relations
+
+This applies to relations between data models of the same module. To retrieve linked records of different modules, use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md).
+
+```ts
+const post = await postModuleService.retrievePost("123", {
+ relations: ["author"],
+})
+```
+
+### Parameters
+
+To retrieve the data model with relations, pass as a second parameter of the method an object with the property `relations`. `relations`'s value is an array of relation names.
+
+### Returns
+
+The method returns the record as an object.
+
+***
+
+## Select Properties to Retrieve
+
+```ts
+const post = await postModuleService.retrievePost("123", {
+ select: ["id", "name"],
+})
+```
+
+### Parameters
+
+By default, all of the record's properties are retrieved. To select specific ones, pass in the second object parameter a `select` property. Its value is an array of property names.
+
+### Returns
+
+The method returns the record as an object.
+
+
# listAndCount Method - Service Factory Reference
This method retrieves a list of records with the total count.
@@ -62727,189 +62864,123 @@ The method returns an array with two items:
2. The second is the total count of records.
-# retrieve Method - Service Factory Reference
+# list Method - Service Factory Reference
-This method retrieves one record of the data model by its ID.
+This method retrieves a list of records.
-## Retrieve a Record
+## Retrieve List of Records
```ts
-const post = await postModuleService.retrievePost("123")
+const posts = await postModuleService.listPosts()
+```
+
+If no parameters are passed, the method returns an array of the first `15` records.
+
+***
+
+## Filter Records
+
+```ts
+const posts = await postModuleService.listPosts({
+ id: ["123", "321"],
+})
```
### Parameters
-Pass the ID of the record to retrieve.
+To retrieve records matching a set of filters, pass an object of the filters as a first parameter.
+
+Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md).
### Returns
-The method returns the record as an object.
+The method returns an array of the first `15` records matching the filters.
***
-## Retrieve a Record's Relations
+## Retrieve Relations
This applies to relations between data models of the same module. To retrieve linked records of different modules, use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md).
```ts
-const post = await postModuleService.retrievePost("123", {
+const posts = await postModuleService.listPosts({}, {
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.
+To retrieve records with their relations, pass as a second parameter an object having a `relations` property. `relations`'s value is an array of relation names.
### Returns
-The method returns the record as an object.
+The method returns an array of the first `15` records matching the filters.
***
-## Select Properties to Retrieve
+## Select Properties
```ts
-const post = await postModuleService.retrievePost("123", {
+const posts = await postModuleService.listPosts({}, {
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.
+By default, retrieved records have all their properties. To select specific properties to retrieve, pass in the second object parameter a `select` property.
+
+`select`'s value is an array of property names to retrieve.
### Returns
-The method returns the record as an object.
-
-
-# delete Method - Service Factory Reference
-
-This method deletes one or more records.
-
-## Delete One Record
-
-```ts
-await postModuleService.deletePosts("123")
-```
-
-To delete one record, pass its ID as a parameter of the method.
+The method returns an array of the first `15` records matching the filters.
***
-## Delete Multiple Records
+## Paginate Relations
```ts
-await postModuleService.deletePosts([
- "123",
- "321",
-])
-```
-
-To delete multiple records, pass an array of IDs as a parameter of the method.
-
-***
-
-## Delete Records Matching Filters
-
-```ts
-await postModuleService.deletePosts({
- name: "My Post",
-})
-```
-
-To delete records matching a set of filters, pass an object of filters as a parameter.
-
-Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md).
-
-
-# softDelete Method - Service Factory Reference
-
-This method soft deletes one or more records of the data model.
-
-## Soft Delete One Record
-
-```ts
-const deletedPosts = await postModuleService.softDeletePosts(
- "123"
-)
-```
-
-### Parameters
-
-To soft delete a record, pass its ID as a parameter of the method.
-
-### Returns
-
-The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of soft-deleted records' IDs.
-
-For example, the returned object of the above example is:
-
-```ts
-deletedPosts = {
- post_id: ["123"],
-}
-```
-
-***
-
-## Soft Delete Multiple Records
-
-```ts
-const deletedPosts = await postModuleService.softDeletePosts([
- "123",
- "321",
-])
-```
-
-### Parameters
-
-To soft delete multiple records, pass an array of IDs as a parameter of the method.
-
-### Returns
-
-The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of soft-deleted records' IDs.
-
-For example, the returned object of the above example is:
-
-```ts
-deletedPosts = {
- post_id: [
- "123",
- "321",
- ],
-}
-```
-
-***
-
-## Soft Delete Records Matching Filters
-
-```ts
-const deletedPosts = await postModuleService.softDeletePosts({
- name: "My Post",
+const posts = await postModuleService.listPosts({}, {
+ take: 20,
+ skip: 10,
})
```
### Parameters
-To soft delete records matching a set of filters, pass an object of filters as a parameter.
+To paginate the returned records, the second object parameter accepts the following properties:
-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).
+- `take`: a number indicating how many records to retrieve. By default, it's `15`.
+- `skip`: a number indicating how many records to skip before the retrieved records. By default, it's `0`.
### Returns
-The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of soft-deleted records' IDs.
+The method returns an array of records. The number of records is less than or equal to `take`'s value.
-For example, the returned object of the above example is:
+***
+
+## Sort Records
```ts
-deletedPosts = {
- post_id: ["123"],
-}
+const posts = await postModuleService.listPosts({}, {
+ order: {
+ name: "ASC",
+ },
+})
```
+### Parameters
+
+To sort records by one or more properties, pass to the second object parameter the `order` property. Its value is an object whose keys are the property names, and values can either be:
+
+- `ASC` to sort by this property in the ascending order.
+- `DESC` to sort by this property in the descending order.
+
+### Returns
+
+The method returns an array of the first `15` records matching the filters.
+
# update Method - Service Factory Reference
@@ -63034,6 +63105,93 @@ Learn more about accepted filters in [this documentation](https://docs.medusajs.
The method returns an array of objects of updated records.
+# softDelete Method - Service Factory Reference
+
+This method soft deletes one or more records of the data model.
+
+## Soft Delete One Record
+
+```ts
+const deletedPosts = await postModuleService.softDeletePosts(
+ "123"
+)
+```
+
+### Parameters
+
+To soft delete a record, pass its ID as a parameter of the method.
+
+### Returns
+
+The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of soft-deleted records' IDs.
+
+For example, the returned object of the above example is:
+
+```ts
+deletedPosts = {
+ post_id: ["123"],
+}
+```
+
+***
+
+## Soft Delete Multiple Records
+
+```ts
+const deletedPosts = await postModuleService.softDeletePosts([
+ "123",
+ "321",
+])
+```
+
+### Parameters
+
+To soft delete multiple records, pass an array of IDs as a parameter of the method.
+
+### Returns
+
+The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of soft-deleted records' IDs.
+
+For example, the returned object of the above example is:
+
+```ts
+deletedPosts = {
+ post_id: [
+ "123",
+ "321",
+ ],
+}
+```
+
+***
+
+## Soft Delete Records Matching Filters
+
+```ts
+const deletedPosts = await postModuleService.softDeletePosts({
+ name: "My Post",
+})
+```
+
+### Parameters
+
+To soft delete records matching a set of filters, pass an object of filters as a parameter.
+
+Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md).
+
+### Returns
+
+The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of soft-deleted records' IDs.
+
+For example, the returned object of the above example is:
+
+```ts
+deletedPosts = {
+ post_id: ["123"],
+}
+```
+
+
# Filter Records - Service Factory Reference
Many of the service factory's generated methods allow passing filters to perform an operation, such as to update or delete records matching the filters.
@@ -63781,168 +63939,6 @@ How to install and setup Medusa UI.
-# Medusa Admin Extension
-
-How to install and use Medusa UI for building Admin extensions.
-
-## Installation
-
-***
-
-The `@medusajs/ui` package is a already installed as a dependency of the `@medusajs/admin` package. Due to this you can simply import the package and use it in your local Admin extensions.
-
-If you are building a Admin extension as part of a Medusa plugin, you can install the package as a dependency of your plugin.
-
-```bash
-npm install @medusajs/ui
-```
-
-## Configuration
-
-***
-
-The configuration of the UI package is handled by the `@medusajs/admin` package. Therefore, you do not need to any additional configuration to use the UI package in your Admin extensions.
-
-
-# Standalone Project
-
-How to install and use Medusa UI in a standalone project.
-
-## Installation
-
-***
-
-Medusa UI is a React UI library and while it's intended for usage within Medusa projects, it can also be used in any React project.
-
-### Install Medusa UI
-
-Install the React UI library with the following command:
-
-```bash
-npm install @medusajs/ui
-```
-
-### Configuring Tailwind CSS
-
-The components are styled using Tailwind CSS, and in order to use them, you will need to install Tailwind CSS in your project as well.
-For more information on how to install Tailwind CSS, please refer to the [Tailwind CSS documentation](https://tailwindcss.com/docs/installation).
-
-All of the classes used for Medusa UI are shipped as a Tailwind CSS customization.
-You can install it with the following command:
-
-```bash
-npm install @medusajs/ui-preset
-```
-
-After you have installed Tailwind CSS and the Medusa UI preset, you need to add the following to your `tailwind.config.js`file:
-
-```tsx
-module.exports = {
- presets: [require("@medusajs/ui-preset")],
- // ...
-}
-```
-
-In order for the styles to be applied correctly to the components, you will also need to ensure that
-`@medusajs/ui` is included in the content field of your `tailwind.config.js` file:
-
-```tsx
-module.exports = {
- content: [
- // ...
- "./node_modules/@medusajs/ui/dist/**/*.{js,jsx,ts,tsx}",
- ],
- // ...
-}
-```
-
-If you are working within a monorepo, you may need to add the path to the `@medusajs/ui` package in your `tailwind.config.js` like so:
-
-```tsx
-const path = require("path")
-
-const uiPath = path.resolve(
- require.resolve("@medusajs/ui"),
- "../..",
- "\*_/_.{js,jsx,ts,tsx}"
-)
-
-module.exports = {
- content: [
- // ...
- uiPath,
- ],
- // ...
-}
-
-```
-
-## Start building
-
-***
-
-You are now ready to start building your application with Medusa UI. You can import the components like so:
-
-```tsx
-import { Button, Drawer } from "@medusajs/ui"
-```
-
-## Updating UI Packages
-
-***
-
-Medusa's design-system packages, including `@medusajs/ui`, `@medusajs/ui-preset`, and `@medusajs/ui-icons`, are versioned independently. However, they're still part of the latest Medusa release. So, you can browse the [release notes](https://github.com/medusajs/medusa/releases) to see if there are any breaking changes to these packages.
-
-To update these packages, update their version in your `package.json` file and re-install dependencies. For example:
-
-```bash
-npm install @medusajs/ui
-```
-
-
-# clx
-
-Utility function for working with classNames.
-
-## Usage
-
-***
-
-The `clx` function is a utility function for working with classNames. It is built using [clsx](https://www.npmjs.com/package/clsx) and [tw-merge](https://www.npmjs.com/package/tw-merge) and is intended to be used with [Tailwind CSS](https://tailwindcss.com/).
-
-```tsx
-import { clx } from "@medusajs/ui"
-
-type BoxProps = {
- className?: string
- children: React.ReactNode
- mt: "sm" | "md" | "lg"
-}
-
-const Box = ({ className, children, mt }: BoxProps) => {
- return (
-
- {children}
-
- )
-}
-
-```
-
-In the above example the utility is used to apply a base style, a margin top that is dependent on the `mt` prop and a custom className.
-The Box component accepts a `className` prop that is merged with the other classNames, and the underlying usage of `tw-merge` ensures that all Tailwind CSS classes are merged without style conflicts.
-
-
# Alert
A component for displaying important messages.
@@ -70362,3 +70358,165 @@ If you're using the `Tooltip` component in a project other than the Medusa Admin
- delayDuration: (number) The duration from when the pointer enters the trigger until the tooltip gets opened. Default: 100
- skipDelayDuration: (number) How much time a user has to enter another trigger without incurring a delay again. Default: 300
- disableHoverableContent: (boolean) When \`true\`, trying to hover the content will result in the tooltip closing as the pointer leaves the trigger.
+
+
+# Medusa Admin Extension
+
+How to install and use Medusa UI for building Admin extensions.
+
+## Installation
+
+***
+
+The `@medusajs/ui` package is a already installed as a dependency of the `@medusajs/admin` package. Due to this you can simply import the package and use it in your local Admin extensions.
+
+If you are building a Admin extension as part of a Medusa plugin, you can install the package as a dependency of your plugin.
+
+```bash
+npm install @medusajs/ui
+```
+
+## Configuration
+
+***
+
+The configuration of the UI package is handled by the `@medusajs/admin` package. Therefore, you do not need to any additional configuration to use the UI package in your Admin extensions.
+
+
+# Standalone Project
+
+How to install and use Medusa UI in a standalone project.
+
+## Installation
+
+***
+
+Medusa UI is a React UI library and while it's intended for usage within Medusa projects, it can also be used in any React project.
+
+### Install Medusa UI
+
+Install the React UI library with the following command:
+
+```bash
+npm install @medusajs/ui
+```
+
+### Configuring Tailwind CSS
+
+The components are styled using Tailwind CSS, and in order to use them, you will need to install Tailwind CSS in your project as well.
+For more information on how to install Tailwind CSS, please refer to the [Tailwind CSS documentation](https://tailwindcss.com/docs/installation).
+
+All of the classes used for Medusa UI are shipped as a Tailwind CSS customization.
+You can install it with the following command:
+
+```bash
+npm install @medusajs/ui-preset
+```
+
+After you have installed Tailwind CSS and the Medusa UI preset, you need to add the following to your `tailwind.config.js`file:
+
+```tsx
+module.exports = {
+ presets: [require("@medusajs/ui-preset")],
+ // ...
+}
+```
+
+In order for the styles to be applied correctly to the components, you will also need to ensure that
+`@medusajs/ui` is included in the content field of your `tailwind.config.js` file:
+
+```tsx
+module.exports = {
+ content: [
+ // ...
+ "./node_modules/@medusajs/ui/dist/**/*.{js,jsx,ts,tsx}",
+ ],
+ // ...
+}
+```
+
+If you are working within a monorepo, you may need to add the path to the `@medusajs/ui` package in your `tailwind.config.js` like so:
+
+```tsx
+const path = require("path")
+
+const uiPath = path.resolve(
+ require.resolve("@medusajs/ui"),
+ "../..",
+ "\*_/_.{js,jsx,ts,tsx}"
+)
+
+module.exports = {
+ content: [
+ // ...
+ uiPath,
+ ],
+ // ...
+}
+
+```
+
+## Start building
+
+***
+
+You are now ready to start building your application with Medusa UI. You can import the components like so:
+
+```tsx
+import { Button, Drawer } from "@medusajs/ui"
+```
+
+## Updating UI Packages
+
+***
+
+Medusa's design-system packages, including `@medusajs/ui`, `@medusajs/ui-preset`, and `@medusajs/ui-icons`, are versioned independently. However, they're still part of the latest Medusa release. So, you can browse the [release notes](https://github.com/medusajs/medusa/releases) to see if there are any breaking changes to these packages.
+
+To update these packages, update their version in your `package.json` file and re-install dependencies. For example:
+
+```bash
+npm install @medusajs/ui
+```
+
+
+# clx
+
+Utility function for working with classNames.
+
+## Usage
+
+***
+
+The `clx` function is a utility function for working with classNames. It is built using [clsx](https://www.npmjs.com/package/clsx) and [tw-merge](https://www.npmjs.com/package/tw-merge) and is intended to be used with [Tailwind CSS](https://tailwindcss.com/).
+
+```tsx
+import { clx } from "@medusajs/ui"
+
+type BoxProps = {
+ className?: string
+ children: React.ReactNode
+ mt: "sm" | "md" | "lg"
+}
+
+const Box = ({ className, children, mt }: BoxProps) => {
+ return (
+
+ {children}
+
+ )
+}
+
+```
+
+In the above example the utility is used to apply a base style, a margin top that is dependent on the `mt` prop and a custom className.
+The Box component accepts a `className` prop that is merged with the other classNames, and the underlying usage of `tw-merge` ensures that all Tailwind CSS classes are merged without style conflicts.