import { CodeTabs, CodeTab } from "docs-ui" export const metadata = { title: `Medusa Examples`, } # {metadata.title} This documentation page has examples of customizations useful for your custom development in the Medusa application. Each section links to the associated documentation page to learn more about it. ## API Routes An API route is a REST API endpoint that exposes commerce features to external applications, such as storefronts, the admin dashboard, or third-party systems. ### Create API Route Create the file `src/api/hello-world/route.ts` with the following content: ```ts title="src/api/hello-world/route.ts" import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" export const GET = ( req: MedusaRequest, res: MedusaResponse ) => { res.json({ message: "[GET] Hello world!", }) } ``` This creates a `GET` API route at `/hello-world`. Learn more in [this documentation](!docs!/learn/fundamentals/api-routes). ### Resolve Resources in API Route To resolve resources from the Medusa container in an API route: ```ts highlights={[["8", "resolve", "Resolve the Product Module's\nmain service from the Medusa container."]]} import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" import { Modules } from "@medusajs/framework/utils" export const GET = async ( req: MedusaRequest, res: MedusaResponse ) => { const productModuleService = req.scope.resolve( Modules.PRODUCT ) const [, count] = await productModuleService .listAndCountProducts() res.json({ count, }) } ``` This resolves the Product Module's main service. Learn more in [this documentation](!docs!/learn/fundamentals/medusa-container). ### Use Path Parameters API routes can accept path parameters. To do that, create the file `src/api/hello-world/[id]/route.ts` with the following content: export const singlePathHighlights = [ ["11", "req.params.id", "Access the path parameter `id`"] ] ```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}!`, }) } ``` Learn more about path parameters in [this documentation](!docs!/learn/fundamentals/api-routes/parameters#path-parameters). ### Use Query Parameters API routes can accept query parameters: export const queryHighlights = [ ["11", "req.query.name", "Access the query parameter `name`"], ] ```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}`, }) } ``` Learn more about query parameters in [this documentation](!docs!/learn/fundamentals/api-routes/parameters#query-parameters). ### Use Body Parameters API routes can accept request body parameters: export const bodyHighlights = [ ["11", "HelloWorldReq", "Specify the type of the request body parameters."], ["15", "req.body.name", "Access the request body parameter `name`"], ] ```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}!`, }) } ``` Learn more about request body parameters in [this documentation](!docs!/learn/fundamentals/api-routes/parameters#request-body-parameters). ### Set Response Code You can change the response code of an API route: ```ts highlights={[["7", "status", "Change the response's status."]]} import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" export const GET = async ( req: MedusaRequest, res: MedusaResponse ) => { res.status(200).json({ message: "Hello, World!", }) } ``` Learn more about setting the response code in [this documentation](!docs!/learn/fundamentals/api-routes/responses#set-response-status-code). ### Execute a Workflow in an API Route To execute a workflow in an API route: ```ts import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" import myWorkflow from "../../workflows/hello-world" export async function GET( req: MedusaRequest, res: MedusaResponse ) { const { result } = await myWorkflow(req.scope) .run({ input: { name: req.query.name as string, }, }) res.send(result) } ``` Learn more in [this documentation](!docs!/learn/fundamentals/workflows#3-execute-the-workflow). ### Change Response Content Type By default, an API route's response has the content type `application/json`. To change it to another content type, use the `writeHead` method of `MedusaResponse`: export const responseContentTypeHighlights = [ ["7", "writeHead", "Change the content type in the header."] ] ```ts highlights={responseContentTypeHighlights} import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" export const GET = async ( req: MedusaRequest, res: MedusaResponse ) => { res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive", }) const interval = setInterval(() => { res.write("Streaming data...\n") }, 3000) req.on("end", () => { clearInterval(interval) res.end() }) } ``` This changes the response type to return an event stream. Learn more in [this documentation](!docs!/learn/fundamentals/api-routes/responses#change-response-content-type). ### Create Middleware A middleware is a function executed when a request is sent to an API Route. Create the file `src/api/middlewares.ts` with the following content: ```ts title="src/api/middlewares.ts" import type { MedusaNextFunction, MedusaRequest, MedusaResponse, defineMiddlewares, } from "@medusajs/framework/http" export default defineMiddlewares({ routes: [ { matcher: "/custom*", middlewares: [ ( req: MedusaRequest, res: MedusaResponse, next: MedusaNextFunction ) => { console.log("Received a request!") next() }, ], }, { matcher: "/custom/:id", middlewares: [ ( req: MedusaRequest, res: MedusaResponse, next: MedusaNextFunction ) => { console.log("With Path Parameter") next() }, ], }, ], }) ``` Learn more about middlewares in [this documentation](!docs!/learn/fundamentals/api-routes/middlewares). ### Restrict HTTP Methods in Middleware To restrict a middleware to an HTTP method: export const middlewareMethodHighlights = [ ["12", "method", "Apply the middleware on `POST` and `PUT` requests only."] ] ```ts title="src/api/middlewares.ts" highlights={middlewareMethodHighlights} import type { MedusaNextFunction, MedusaRequest, MedusaResponse, defineMiddlewares, } from "@medusajs/framework/http" export default defineMiddlewares({ routes: [ { matcher: "/custom*", method: ["POST", "PUT"], middlewares: [ // ... ], }, ], }) ``` ### Add Validation for Custom Routes 1. Create a [Zod](https://zod.dev/) schema in the file `src/api/custom/validators.ts`: ```ts title="src/api/custom/validators.ts" import { z } from "zod" export const PostStoreCustomSchema = z.object({ a: z.number(), b: z.number(), }) ``` 2. Add a validation middleware to the custom route in `src/api/middlewares.ts`: ```ts title="src/api/middlewares.ts" highlights={[["13", "validateAndTransformBody"]]} import { validateAndTransformBody, defineMiddlewares, } from "@medusajs/framework/http" import { PostStoreCustomSchema } from "./custom/validators" export default defineMiddlewares({ routes: [ { matcher: "/custom", method: "POST", middlewares: [ validateAndTransformBody(PostStoreCustomSchema), ], }, ], }) ``` 3. Use the validated body in the `/custom` API route: ```ts title="src/api/custom/route.ts" highlights={[["14", "validatedBody"]]} import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" import { z } from "zod" import { PostStoreCustomSchema } from "./validators" type PostStoreCustomSchemaType = z.infer< typeof PostStoreCustomSchema > export const POST = async ( req: MedusaRequest, res: MedusaResponse ) => { res.json({ sum: req.validatedBody.a + req.validatedBody.b, }) } ``` Learn more about request body validation in [this documentation](!docs!/learn/fundamentals/api-routes/validation). ### Pass Additional Data to API Route In this example, you'll pass additional data to the Create Product API route, then consume its hook: Find this example in details in [this documentation](!docs!/learn/customization/extend-features/extend-create-product). 1. Create the file `src/api/middlewares.ts` with the following content: ```ts title="src/api/middlewares.ts" highlights={[["10", "brand_id", "Replace with your custom field."]]} import { defineMiddlewares } from "@medusajs/framework/http" import { z } from "zod" export default defineMiddlewares({ routes: [ { matcher: "/admin/products", method: ["POST"], additionalDataValidator: { brand_id: z.string().optional(), }, }, ], }) ``` Learn more about additional data in [this documentation](!docs!/learn/fundamentals/api-routes/additional-data). 2. Create the file `src/workflows/hooks/created-product.ts` with the following content: ```ts import { createProductsWorkflow } from "@medusajs/medusa/core-flows" import { StepResponse } from "@medusajs/framework/workflows-sdk" createProductsWorkflow.hooks.productsCreated( (async ({ products, additional_data }, { container }) => { if (!additional_data.brand_id) { return new StepResponse([], []) } // TODO perform custom action }), (async (links, { container }) => { // TODO undo the action in the compensation }) ) ``` Learn more about workflow hooks in [this documentation](!docs!/learn/fundamentals/workflows/workflow-hooks). ### Restrict an API Route to Admin Users You can protect API routes by restricting access to authenticated admin users only. Add the following middleware in `src/api/middlewares.ts`: ```ts title="src/api/middlewares.ts" highlights={[["11", "authenticate"]]} import { defineMiddlewares, authenticate, } from "@medusajs/framework/http" export default defineMiddlewares({ routes: [ { matcher: "/custom/admin*", middlewares: [ authenticate( "user", ["session", "bearer", "api-key"] ), ], }, ], }) ``` Learn more in [this documentation](!docs!/learn/fundamentals/api-routes/protected-routes). ### Restrict an API Route to Logged-In Customers You can protect API routes by restricting access to authenticated customers only. Add the following middleware in `src/api/middlewares.ts`: ```ts title="src/api/middlewares.ts" highlights={[["11", "authenticate"]]} import { defineMiddlewares, authenticate, } from "@medusajs/framework/http" export default defineMiddlewares({ routes: [ { matcher: "/custom/customer*", middlewares: [ authenticate("customer", ["session", "bearer"]), ], }, ], }) ``` Learn more in [this documentation](!docs!/learn/fundamentals/api-routes/protected-routes). ### Retrieve Logged-In Admin User To retrieve the currently logged-in user in an API route: Requires setting up the authentication middleware as explained in [this example](#restrict-an-api-route-to-admin-users). ```ts highlights={[["16", "req.auth_context.actor_id", "Access the user's ID."]]} import type { AuthenticatedMedusaRequest, MedusaResponse, } from "@medusajs/framework/http" import { Modules } from "@medusajs/framework/utils" export const GET = async ( req: AuthenticatedMedusaRequest, res: MedusaResponse ) => { const userModuleService = req.scope.resolve( Modules.USER ) const user = await userModuleService.retrieveUser( req.auth_context.actor_id ) // ... } ``` Learn more in [this documentation](!docs!/learn/fundamentals/api-routes/protected-routes#retrieve-logged-in-admin-users-details). ### Retrieve Logged-In Customer To retrieve the currently logged-in customer in an API route: Requires setting up the authentication middleware as explained in [this example](#restrict-an-api-route-to-logged-in-customers). ```ts highlights={[["18", "req.auth_context.actor_id", "Access the customer's ID."]]} import type { AuthenticatedMedusaRequest, MedusaResponse, } from "@medusajs/framework/http" import { Modules } from "@medusajs/framework/utils" export const GET = async ( req: AuthenticatedMedusaRequest, res: MedusaResponse ) => { if (req.auth_context?.actor_id) { // retrieve customer const customerModuleService = req.scope.resolve( Modules.CUSTOMER ) const customer = await customerModuleService.retrieveCustomer( req.auth_context.actor_id ) } // ... } ``` Learn more in [this documentation](!docs!/learn/fundamentals/api-routes/protected-routes#retrieve-logged-in-customers-details). ### Throw Errors in API Route To throw errors in an API route, use `MedusaError` from the Medusa Framework: ```ts highlights={[["9", "MedusaError"]]} 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." ) } // ... } ``` Learn more in [this documentation](!docs!/learn/fundamentals/api-routes/errors). ### Override Error Handler of API Routes To override the error handler of API routes, create the file `src/api/middlewares.ts` with the following content: ```ts title="src/api/middlewares.ts" highlights={[["10", "errorHandler"]]} 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.", }) }, }) ``` Learn more in [this documentation](!docs!/learn/fundamentals/api-routes/errors#override-error-handler), ### Setting up CORS for Custom API Routes By default, Medusa configures CORS for all routes starting with `/admin`, `/store`, and `/auth`. To configure CORS for routes under other prefixes, create the file `src/api/middlewares.ts` with the following content: ```ts title="src/api/middlewares.ts" import type { MedusaNextFunction, MedusaRequest, MedusaResponse, defineMiddlewares, } from "@medusajs/framework/http" import { ConfigModule } from "@medusajs/framework/types" import { parseCorsOrigins } from "@medusajs/framework/utils" import cors from "cors" export default defineMiddlewares({ routes: [ { matcher: "/custom*", middlewares: [ ( req: MedusaRequest, res: MedusaResponse, next: MedusaNextFunction ) => { const configModule: ConfigModule = req.scope.resolve("configModule") return cors({ origin: parseCorsOrigins( configModule.projectConfig.http.storeCors ), credentials: true, })(req, res, next) }, ], }, ], }) ``` ### Parse Webhook Body By default, the Medusa application parses a request's body using JSON. To parse a webhook's body, create the file `src/api/middlewares.ts` with the following content: ```ts title="src/api/middlewares.ts" highlights={[["9"]]} import { defineMiddlewares, } from "@medusajs/framework/http" export default defineMiddlewares({ routes: [ { matcher: "/webhooks/*", bodyParser: { preserveRawBody: true }, method: ["POST"], }, ], }) ``` To access the raw body data in your route, use the `req.rawBody` property: ```ts title="src/api/webhooks/route.ts" import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" export const POST = ( req: MedusaRequest, res: MedusaResponse ) => { console.log(req.rawBody) } ``` --- ## Modules A module is a package of reusable commerce or architectural functionalities. They handle business logic in a class called a service, and define and manage data models that represent tables in the database. ### Create Module Find this example explained in details in [this documentation](!docs!/learn/fundamentals/modules). 1. Create the directory `src/modules/blog`. 2. Create the file `src/modules/blog/models/post.ts` with the following data model: ```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 ``` 3. Create the file `src/modules/blog/service.ts` with the following service: ```ts title="src/modules/blog/service.ts" import { MedusaService } from "@medusajs/framework/utils" import Post from "./models/post" class BlogModuleService extends MedusaService({ Post, }){ } export default BlogModuleService ``` 4. Create the file `src/modules/blog/index.ts` that exports the module definition: ```ts title="src/modules/blog/index.ts" import BlogModuleService from "./service" import { Module } from "@medusajs/framework/utils" export const BLOG_MODULE = "blog" export default Module(BLOG_MODULE, { service: BlogModuleService, }) ``` 5. Add the module to the configurations in `medusa-config.ts`: ```ts title="medusa-config.ts" module.exports = defineConfig({ projectConfig: { // ... }, modules: [ { resolve: "./modules/blog", }, ], }) ``` 6. Generate and run migrations: ```bash npx medusa db:generate blog npx medusa db:migrate ``` 7. Use the module's main service in an API route: ```ts title="src/api/custom/route.ts" import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" import BlogModuleService from "../../modules/blog/service" import { BLOG_MODULE } from "../../modules/blog" export async function GET( req: MedusaRequest, res: MedusaResponse ): Promise { const blogModuleService: BlogModuleService = req.scope.resolve( BLOG_MODULE ) const post = await blogModuleService.createPosts({ title: "test", }) res.json({ post, }) } ``` ### Module with Multiple Services To add services in your module other than the main one, create them in the `services` directory of the module. For example, create the file `src/modules/blog/services/category.ts` with the following content: ```ts title="src/modules/blog/services/category.ts" export class CategoryService { // TODO add methods } ``` Then, export the service in the file `src/modules/blog/services/index.ts`: ```ts title="src/modules/blog/services/index.ts" export * from "./category" ``` Finally, resolve the service in your module's main service or loader: ```ts title="src/modules/blog/service.ts" import { MedusaService } from "@medusajs/framework/utils" import Post from "./models/post" import { CategoryService } from "./services" type InjectedDependencies = { categoryService: CategoryService } class BlogModuleService extends MedusaService({ Post, }){ private categoryService: CategoryService constructor({ categoryService }: InjectedDependencies) { super(...arguments) this.categoryService = categoryService } } export default BlogModuleService ``` Learn more in [this documentation](!docs!/learn/fundamentals/modules/multiple-services). ### Accept Module Options A module can accept options for configurations and secrets. To accept options in your module: 1. Pass options to the module in `medusa-config.ts`: ```ts title="medusa-config.ts" highlights={[["6", "options"]]} module.exports = defineConfig({ // ... modules: [ { resolve: "./modules/blog", options: { apiKey: true, }, }, ], }) ``` 2. Access the options in the module's main service: ```ts title="src/modules/blog/service.ts" highlights={[["14", "options"]]} import { MedusaService } from "@medusajs/framework/utils" import Post from "./models/post" // recommended to define type in another file type ModuleOptions = { apiKey?: boolean } export default class BlogModuleService extends MedusaService({ Post, }){ protected options_: ModuleOptions constructor({}, options?: ModuleOptions) { super(...arguments) this.options_ = options || { apiKey: false, } } // ... } ``` Learn more in [this documentation](!docs!/learn/fundamentals/modules/options). ### Integrate Third-Party System in Module An example of integrating a dummy third-party system in a module's service: ```ts title="src/modules/blog/service.ts" import { Logger } from "@medusajs/framework/types" import { BLOG_MODULE } from ".." export type ModuleOptions = { apiKey: string } type InjectedDependencies = { logger: Logger } export class BlogClient { private options_: ModuleOptions private logger_: Logger constructor( { logger }: InjectedDependencies, options: ModuleOptions ) { this.logger_ = logger this.options_ = options } private async sendRequest(url: string, method: string, data?: any) { this.logger_.info(`Sending a ${ method } request to ${url}. data: ${JSON.stringify(data, null, 2)}`) this.logger_.info(`Client Options: ${ JSON.stringify(this.options_, null, 2) }`) } } ``` Find a longer example of integrating a third-party service in [this documentation](!docs!/learn/customization/integrate-systems/service). --- ## Data Models A data model represents a table in the database. Medusa provides a data model language to intuitively create data models. ### Create Data Model To create a data model in a module: This assumes you already have a module. If not, follow [this example](#create-module). 1. Create the file `src/modules/blog/models/post.ts` with the following data model: ```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 ``` 2. Generate and run migrations: ```bash npx medusa db:generate blog npx medusa db:migrate ``` Learn more in [this documentation](!docs!/learn/fundamentals/modules#1-create-data-model). ### Data Model Property Types A data model can have properties of the following types: 1. ID property: ```ts const Post = model.define("post", { id: model.id(), // ... }) ``` 2. Text property: ```ts const Post = model.define("post", { title: model.text(), // ... }) ``` 3. Number property: ```ts const Post = model.define("post", { views: model.number(), // ... }) ``` 4. Big Number property: ```ts const Post = model.define("post", { price: model.bigNumber(), // ... }) ``` 5. Boolean property: ```ts const Post = model.define("post", { isPublished: model.boolean(), // ... }) ``` 6. Enum property: ```ts const Post = model.define("post", { status: model.enum(["draft", "published"]), // ... }) ``` 7. Date-Time property: ```ts const Post = model.define("post", { publishedAt: model.dateTime(), // ... }) ``` 8. JSON property: ```ts const Post = model.define("post", { metadata: model.json(), // ... }) ``` 9. Array property: ```ts const Post = model.define("post", { tags: model.array(), // ... }) ``` Learn more in [this documentation](!docs!/learn/fundamentals/data-models/properties). ### Set Primary Key To set an `id` property as the primary key of a data model: ```ts highlights={[["4", "primaryKey"]]} import { model } from "@medusajs/framework/utils" const Post = model.define("post", { id: model.id().primaryKey(), // ... }) export default Post ``` To set a `text` property as the primary key: ```ts highlights={[["4", "primaryKey"]]} import { model } from "@medusajs/framework/utils" const Post = model.define("post", { title: model.text().primaryKey(), // ... }) export default Post ``` To set a `number` property as the primary key: ```ts highlights={[["4", "primaryKey"]]} import { model } from "@medusajs/framework/utils" const Post = model.define("post", { views: model.number().primaryKey(), // ... }) export default Post ``` Learn more in [this documentation](!docs!/learn/fundamentals/data-models/properties#set-primary-key-property). ### Default Property Value To set the default value of a property: ```ts highlights={[["6"], ["9"]]} import { model } from "@medusajs/framework/utils" const Post = model.define("post", { status: model .enum(["draft", "published"]) .default("draft"), views: model .number() .default(0), // ... }) export default Post ``` Learn more in [this documentation](!docs!/learn/fundamentals/data-models/properties#property-default-value). ### Nullable Property To allow `null` values for a property: ```ts highlights={[["4", "nullable"]]} import { model } from "@medusajs/framework/utils" const Post = model.define("post", { price: model.bigNumber().nullable(), // ... }) export default Post ``` Learn more in [this documentation](!docs!/learn/fundamentals/data-models/properties#make-property-optional). ### Unique Property To create a unique index on a property: ```ts highlights={[["4", "unique"]]} import { model } from "@medusajs/framework/utils" const Post = model.define("post", { title: model.text().unique(), // ... }) export default Post ``` Learn more in [this documentation](!docs!/learn/fundamentals/data-models/properties#unique-property). ### Define Database Index on Property To define a database index on a property: ```ts highlights={[["5", "index"]]} import { model } from "@medusajs/framework/utils" const MyCustom = model.define("my_custom", { id: model.id().primaryKey(), title: model.text().index( "IDX_POST_TITLE" ), }) export default MyCustom ``` Learn more in [this documentation](!docs!/learn/fundamentals/data-models/properties#define-database-index-on-property). ### Define Composite Index on Data Model To define a composite index on a data model: ```ts highlights={[["7", "indexes"]]} import { model } from "@medusajs/framework/utils" const MyCustom = model.define("my_custom", { id: model.id().primaryKey(), name: model.text(), age: model.number().nullable(), }).indexes([ { on: ["name", "age"], where: { age: { $ne: null, }, }, }, ]) export default MyCustom ``` Learn more in [this documentation](!docs!/learn/fundamentals/data-models/index). ### Make a Property Searchable To make a property searchable using terms or keywords: ```ts highlights={[["4", "searchable"]]} import { model } from "@medusajs/framework/utils" const Post = model.define("post", { title: model.text().searchable(), // ... }) export default Post ``` Then, to search by that property, pass the `q` filter to the `list` or `listAndCount` generated methods of the module's main service: `blogModuleService` is the main service that manages the `Post` data model. ```ts const posts = await blogModuleService.listPosts({ q: "John", }) ``` Learn more in [this documentation](!docs!/learn/fundamentals/data-models/properties#searchable-property). ### Create One-to-One Relationship The following creates a one-to-one relationship between the `User` and `Email` data models: ```ts highlights={[["5", "hasOne"], ["12", "belongsTo"]]} import { model } from "@medusajs/framework/utils" const User = model.define("user", { id: model.id().primaryKey(), email: model.hasOne(() => Email, { mappedBy: "user", }), }) const Email = model.define("email", { id: model.id().primaryKey(), user: model.belongsTo(() => User, { mappedBy: "email", }), }) ``` Learn more in [this documentation](!docs!/learn/fundamentals/data-models/relationships#one-to-one-relationship). ### Create One-to-Many Relationship The following creates a one-to-many relationship between the `Store` and `Product` data models: ```ts highlights={[["5", "hasMany"], ["12", "belongsTo"]]} import { model } from "@medusajs/framework/utils" const Store = model.define("store", { id: model.id().primaryKey(), products: model.hasMany(() => Product, { mappedBy: "store", }), }) const Product = model.define("product", { id: model.id().primaryKey(), store: model.belongsTo(() => Store, { mappedBy: "products", }), }) ``` Learn more in [this documentation](!docs!/learn/fundamentals/data-models/relationships#one-to-many-relationship). ### Create Many-to-Many Relationship The following creates a many-to-many relationship between the `Order` and `Product` data models: ```ts highlights={[["5", "manyToMany"], ["12", "manyToMany"]]} import { model } from "@medusajs/framework/utils" const Order = model.define("order", { id: model.id().primaryKey(), products: model.manyToMany(() => Product, { mappedBy: "orders", }), }) const Product = model.define("product", { id: model.id().primaryKey(), orders: model.manyToMany(() => Order, { mappedBy: "products", }), }) ``` Learn more in [this documentation](!docs!/learn/fundamentals/data-models/relationships#many-to-many-relationship). ### Configure Cascades of Data Model To configure cascade on a data model: ```ts highlights={[["10", "cascades"]]} import { model } from "@medusajs/framework/utils" import Product from "./product" const Store = model.define("store", { id: model.id().primaryKey(), products: model.hasMany(() => Product, { mappedBy: "store", }), }) .cascades({ delete: ["products"], }) ``` This configures the delete cascade on the `Store` data model so that, when a store is delete, its products are also deleted. Learn more in [this documentation](!docs!/learn/fundamentals/data-models/relationships#cascades). ### Manage One-to-One Relationship Consider you have a one-to-one relationship between `Email` and `User` data models, where an email belongs to a user. To set the ID of the user that an email belongs to: `blogModuleService` is the main service that manages the `Email` and `User` data models. ```ts // when creating an email const email = await blogModuleService.createEmails({ // other properties... user: "123", }) // when updating an email const email = await blogModuleService.updateEmails({ id: "321", // other properties... user: "123", }) ``` And to set the ID of a user's email when creating or updating it: ```ts // when creating a user const user = await blogModuleService.createUsers({ // other properties... email: "123", }) // when updating a user const user = await blogModuleService.updateUsers({ id: "321", // other properties... email: "123", }) ``` Learn more in [this documentation](!docs!/learn/fundamentals/data-models/manage-relationships#manage-one-to-one-relationship). ### Manage One-to-Many Relationship Consider you have a one-to-many relationship between `Product` and `Store` data models, where a store has many products. To set the ID of the store that a product belongs to: `blogModuleService` is the main service that manages the `Product` and `Store` data models. ```ts // when creating a product const product = await blogModuleService.createProducts({ // other properties... store_id: "123", }) // when updating a product const product = await blogModuleService.updateProducts({ id: "321", // other properties... store_id: "123", }) ``` Learn more in [this documentation](!docs!/learn/fundamentals/data-models/manage-relationships#manage-one-to-many-relationship) ### Manage Many-to-Many Relationship Consider you have a many-to-many relationship between `Order` and `Product` data models. To set the orders a product has when creating it: `blogModuleService` is the main service that manages the `Product` and `Order` data models. ```ts const product = await blogModuleService.createProducts({ // other properties... orders: ["123", "321"], }) ``` To add new orders to a product without removing the previous associations: ```ts const product = await blogModuleService.retrieveProduct( "123", { relations: ["orders"], } ) const updatedProduct = await blogModuleService.updateProducts({ id: product.id, // other properties... orders: [ ...product.orders.map((order) => order.id), "321", ], }) ``` Learn more in [this documentation](!docs!/learn/fundamentals/data-models/manage-relationships#manage-many-to-many-relationship). ### Retrieve Related Records To retrieve records related to a data model's records through a relation, pass the `relations` field to the `list`, `listAndCount`, or `retrieve` generated methods: `blogModuleService` is the main service that manages the `Product` and `Order` data models. ```ts highlights={[["4", "relations"]]} const product = await blogModuleService.retrieveProducts( "123", { relations: ["orders"], } ) ``` Learn more in [this documentation](!docs!/learn/fundamentals/data-models/manage-relationships#retrieve-records-of-relation). --- ## Services A service is the main resource in a module. It manages the records of your custom data models in the database, or integrate third-party systems. ### Extend Service Factory The service factory `MedusaService` generates data-management methods for your data models. To extend the service factory in your module's service: ```ts highlights={[["4", "MedusaService"]]} import { MedusaService } from "@medusajs/framework/utils" import Post from "./models/post" class BlogModuleService extends MedusaService({ Post, }){ // TODO implement custom methods } export default BlogModuleService ``` The `BlogModuleService` will now have data-management methods for `Post`. Refer to [this reference](../service-factory-reference/page.mdx) for details on the generated methods. Learn more about the service factory in [this documentation](!docs!/learn/fundamentals/modules/service-factory). ### Resolve Resources in the Service To resolve resources from the module's container in a service: ```ts highlights={[["14"]]} import { Logger } from "@medusajs/framework/types" import { MedusaService } from "@medusajs/framework/utils" import Post from "./models/post" type InjectedDependencies = { logger: Logger } class BlogModuleService extends MedusaService({ Post, }){ protected logger_: Logger constructor({ logger }: InjectedDependencies) { super(...arguments) this.logger_ = logger this.logger_.info("[BlogModuleService]: Hello World!") } // ... } export default BlogModuleService ``` ```ts highlights={[["10"]]} 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!") } // ... } ``` Learn more in [this documentation](!docs!/learn/fundamentals/modules/container). ### Access Module Options in Service To access options passed to a module in its service: ```ts highlights={[["14", "options"]]} import { MedusaService } from "@medusajs/framework/utils" import Post from "./models/post" // recommended to define type in another file type ModuleOptions = { apiKey?: boolean } export default class BlogModuleService extends MedusaService({ Post, }){ protected options_: ModuleOptions constructor({}, options?: ModuleOptions) { super(...arguments) this.options_ = options || { apiKey: "", } } // ... } ``` Learn more in [this documentation](!docs!/learn/fundamentals/modules/options). ### Run Database Query in Service To run database query in your service: ```ts highlights={[["14", "count"], ["21", "execute"]]} // other imports... import { InjectManager, MedusaContext, } from "@medusajs/framework/utils" class BlogModuleService { // ... @InjectManager() async getCount( @MedusaContext() sharedContext?: Context ): Promise { return await sharedContext.manager.count("post") } @InjectManager() async getCountSql( @MedusaContext() sharedContext?: Context ): Promise { const data = await sharedContext.manager.execute( "SELECT COUNT(*) as num FROM post" ) return parseInt(data[0].num) } } ``` Learn more in [this documentation](!docs!/learn/fundamentals/modules/db-operations#run-queries) ### Execute Database Operations in Transactions To execute database operations within a transaction in your service: ```ts import { InjectManager, InjectTransactionManager, MedusaContext, } from "@medusajs/framework/utils" import { Context } from "@medusajs/framework/types" import { EntityManager } from "@medusajs/framework/mikro-orm/knex" class BlogModuleService { // ... @InjectTransactionManager() protected async update_( input: { id: string, name: string }, @MedusaContext() sharedContext?: Context ): Promise { const transactionManager = sharedContext.transactionManager await transactionManager.nativeUpdate( "post", { id: input.id, }, { name: input.name, } ) // retrieve again const updatedRecord = await transactionManager.execute( `SELECT * FROM post WHERE id = '${input.id}'` ) return updatedRecord } @InjectManager() async update( input: { id: string, name: string }, @MedusaContext() sharedContext?: Context ) { return await this.update_(input, sharedContext) } } ``` Learn more in [this documentation](!docs!/learn/fundamentals/modules/db-operations#execute-operations-in-transactions). --- ## Module Links A module link forms an association between two data models of different modules, while maintaining module isolation. ### Define a Link To define a link between your custom module and a Commerce Module, such as the Product Module: 1. Create the file `src/links/blog-product.ts` with the following content: ```ts title="src/links/blog-product.ts" import BlogModule from "../modules/blog" import ProductModule from "@medusajs/medusa/product" import { defineLink } from "@medusajs/framework/utils" export default defineLink( ProductModule.linkable.product, BlogModule.linkable.post ) ``` 2. Run the following command to sync the links: ```bash npx medusa db:migrate ``` Learn more in [this documentation](!docs!/learn/fundamentals/module-links). ### Define a List Link To define a list link, where multiple records of a model can be linked to a record in another: ```ts highlights={[["9", "isList"]]} import BlogModule from "../modules/blog" import ProductModule from "@medusajs/medusa/product" import { defineLink } from "@medusajs/framework/utils" export default defineLink( ProductModule.linkable.product, { linkable: BlogModule.linkable.post, isList: true, } ) ``` Learn more about list links in [this documentation](!docs!/learn/fundamentals/module-links#define-a-list-link). ### Set Delete Cascade on Link Definition To ensure a model's records linked to another model are deleted when the linked model is deleted: ```ts highlights={[["9", "deleteCascades"]]} import BlogModule from "../modules/blog" import ProductModule from "@medusajs/medusa/product" import { defineLink } from "@medusajs/framework/utils" export default defineLink( ProductModule.linkable.product, { linkable: BlogModule.linkable.post, deleteCascades: true, } ) ``` Learn more in [this documentation](!docs!/learn/fundamentals/module-links#define-a-list-link). ### Add Custom Columns to Module Link To add a custom column to the table that stores the linked records of two data models: ```ts highlights={[["9", "database"]]} import BlogModule from "../modules/blog" import ProductModule from "@medusajs/medusa/product" import { defineLink } from "@medusajs/framework/utils" export default defineLink( ProductModule.linkable.product, BlogModule.linkable.post, { database: { extraColumns: { metadata: { type: "json", }, }, }, } ) ``` Then, to set the custom column when creating or updating a link between records: ```ts await link.create({ [Modules.PRODUCT]: { product_id: "123", }, HELLO_MODULE: { my_custom_id: "321", }, data: { metadata: { test: true, }, }, }) ``` To retrieve the custom column when retrieving linked records using Query: ```ts import productBlogLink from "../links/product-blog" // ... const { data } = await query.graph({ entity: productBlogLink.entryPoint, fields: ["metadata", "product.*", "post.*"], filters: { product_id: "prod_123", }, }) ``` Learn more in [this documentation](!docs!/learn/fundamentals/module-links/custom-columns). ### Create Link Between Records To create a link between two records using Link: ```ts import { Modules } from "@medusajs/framework/utils" import { BLOG_MODULE } from "../../modules/blog" // ... await link.create({ [Modules.PRODUCT]: { product_id: "prod_123", }, [HELLO_MODULE]: { my_custom_id: "mc_123", }, }) ``` Learn more in [this documentation](!docs!/learn/fundamentals/module-links/link#create-link). ### Dismiss Link Between Records To dismiss links between records using Link: ```ts import { Modules } from "@medusajs/framework/utils" import { BLOG_MODULE } from "../../modules/blog" // ... await link.dismiss({ [Modules.PRODUCT]: { product_id: "prod_123", }, [BLOG_MODULE]: { post_id: "mc_123", }, }) ``` Learn more in [this documentation](!docs!/learn/fundamentals/module-links/link#dismiss-link). ### Cascade Delete Linked Records To cascade delete records linked to a deleted record: ```ts import { Modules } from "@medusajs/framework/utils" // ... await productModuleService.deleteVariants([variant.id]) await link.delete({ [Modules.PRODUCT]: { product_id: "prod_123", }, }) ``` Learn more in [this documentation](!docs!/learn/fundamentals/module-links/link#cascade-delete-linked-records). ### Restore Linked Records To restore records that were soft-deleted because they were linked to a soft-deleted record: ```ts import { Modules } from "@medusajs/framework/utils" // ... await productModuleService.restoreProducts(["prod_123"]) await link.restore({ [Modules.PRODUCT]: { product_id: "prod_123", }, }) ``` Learn more in [this documentation](!docs!/learn/fundamentals/module-links/link#restore-linked-records). --- ## Query Query fetches data across modules. It’s a set of methods registered in the Medusa container under the `query` key. ### Retrieve Records of Data Model To retrieve records using Query in an API route: ```ts highlights={[["15", "graph"]]} 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: myCustoms } = await query.graph({ entity: "my_custom", fields: ["id", "name"], }) res.json({ my_customs: myCustoms }) } ``` Learn more in [this documentation](!docs!/learn/fundamentals/module-links/query). ### Retrieve Linked Records of Data Model To retrieve records linked to a data model: ```ts highlights={[["20"]]} 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: myCustoms } = await query.graph({ entity: "my_custom", fields: [ "id", "name", "product.*", ], }) res.json({ my_customs: myCustoms }) } ``` Learn more in [this documentation](!docs!/learn/fundamentals/module-links/query#retrieve-linked-records). ### Apply Filters to Retrieved Records To filter the retrieved records: ```ts highlights={[["18", "filters"]]} 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: myCustoms } = await query.graph({ entity: "my_custom", fields: ["id", "name"], filters: { id: [ "mc_01HWSVWR4D2XVPQ06DQ8X9K7AX", "mc_01HWSVWK3KYHKQEE6QGS2JC3FX", ], }, }) res.json({ my_customs: myCustoms }) } ``` Learn more in [this documentation](!docs!/learn/fundamentals/module-links/query#apply-filters). ### Apply Pagination and Sort Records To paginate and sort retrieved records: ```ts highlights={[["21", "pagination"]]} 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: myCustoms, metadata: { count, take, skip } = {}, } = await query.graph({ entity: "my_custom", fields: ["id", "name"], pagination: { skip: 0, take: 10, order: { name: "DESC", }, }, }) res.json({ my_customs: myCustoms, count, take, skip, }) } ``` Learn more in [this documentation](!docs!/learn/fundamentals/module-links/query#sort-records). --- ## Workflows A workflow is a series of queries and actions that complete a task. A workflow allows you to track its execution's progress, provide roll-back logic for each step to mitigate data inconsistency when errors occur, automatically retry failing steps, and more. ### Create a Workflow To create a workflow: 1. Create the first step at `src/workflows/hello-world/steps/step-1.ts` with the following content: ```ts title="src/workflows/hello-world/steps/step-1.ts" import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" export const step1 = createStep("step-1", async () => { return new StepResponse(`Hello from step one!`) }) ``` 2. Create the second step at `src/workflows/hello-world/steps/step-2.ts` with the following content: ```ts title="src/workflows/hello-world/steps/step-2.ts" import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" type StepInput = { name: string } export const step2 = createStep( "step-2", async ({ name }: StepInput) => { return new StepResponse(`Hello ${name} from step two!`) } ) ``` 3. Create the workflow at `src/workflows/hello-world/index.ts` with the following content: ```ts title="src/workflows/hello-world/index.ts" import { createWorkflow, WorkflowResponse, } from "@medusajs/framework/workflows-sdk" import { step1 } from "./steps/step-1" import { step2 } from "./steps/step-2" const myWorkflow = createWorkflow( "hello-world", function (input: WorkflowInput) { const str1 = step1() // to pass input const str2 = step2(input) return new WorkflowResponse({ message: str1, }) } ) export default myWorkflow ``` Learn more in [this documentation](!docs!/learn/fundamentals/workflows). ### Execute a Workflow ```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"], ["13"], ["14"], ["15"], ["16"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" import myWorkflow from "../../workflows/hello-world" export async function GET( req: MedusaRequest, res: MedusaResponse ) { const { result } = await myWorkflow(req.scope) .run({ input: { name: req.query.name as string, }, }) res.send(result) } ``` ```ts title="src/subscribers/customer-created.ts" highlights={[["20"], ["21"], ["22"], ["23"], ["24"], ["25"]]} collapsibleLines="1-9" expandButtonLabel="Show Imports" import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" import myWorkflow from "../workflows/hello-world" import { Modules } from "@medusajs/framework/utils" import { IUserModuleService } from "@medusajs/framework/types" export default async function handleCustomerCreate({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { const userId = data.id const userModuleService: IUserModuleService = container.resolve( Modules.USER ) const user = await userModuleService.retrieveUser(userId) const { result } = await myWorkflow(container) .run({ input: { name: user.first_name, }, }) console.log(result) } export const config: SubscriberConfig = { event: "user.created", } ``` ```ts title="src/jobs/message-daily.ts" highlights={[["7"], ["8"], ["9"], ["10"], ["11"], ["12"]]} import { MedusaContainer } from "@medusajs/framework/types" import myWorkflow from "../workflows/hello-world" export default async function myCustomJob( container: MedusaContainer ) { const { result } = await myWorkflow(container) .run({ input: { name: "John", }, }) console.log(result.message) } export const config = { name: "run-once-a-day", schedule: `0 0 * * *`, }; ``` Learn more in [this documentation](!docs!/learn/fundamentals/workflows#3-execute-the-workflow). ### Step with a Compensation Function Pass a compensation function that undoes what a step did as a second parameter to `createStep`: ```ts highlights={[["15"]]} import { createStep, StepResponse, } from "@medusajs/framework/workflows-sdk" const step1 = createStep( "step-1", async () => { const message = `Hello from step one!` console.log(message) return new StepResponse(message) }, async () => { console.log("Oops! Rolling back my changes...") } ) ``` Learn more in [this documentation](!docs!/learn/fundamentals/workflows/compensation-function). ### Manipulate Variables in Workflow To manipulate variables within a workflow's constructor function, use `transform` from the Workflows SDK: ```ts highlights={[["14", "transform"]]} import { createWorkflow, WorkflowResponse, transform, } from "@medusajs/framework/workflows-sdk" // step imports... const myWorkflow = createWorkflow( "hello-world", function (input) { const str1 = step1(input) const str2 = step2(input) const str3 = transform( { str1, str2 }, (data) => `${data.str1}${data.str2}` ) return new WorkflowResponse(str3) } ) ``` Learn more in [this documentation](!docs!/learn/fundamentals/workflows/variable-manipulation) ### Using Conditions in Workflow To perform steps or set a variable's value based on a condition, use `when-then` from the Workflows SDK: ```ts highlights={[["14", "when"]]} import { createWorkflow, WorkflowResponse, when, } from "@medusajs/framework/workflows-sdk" // step imports... const workflow = createWorkflow( "workflow", function (input: { is_active: boolean }) { const result = when( input, (input) => { return input.is_active } ).then(() => { return isActiveStep() }) // executed without condition const anotherStepResult = anotherStep(result) return new WorkflowResponse( anotherStepResult ) } ) ``` ### Run Workflow in Another To run a workflow in another, use the workflow's `runAsStep` special method: ```ts highlights={[["11", "runAsStep"]]} 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: [ // ... ], }, }) // ... } ) ``` Learn more in [this documentation](!docs!/learn/fundamentals/workflows/execute-another-workflow). ### Consume a Workflow Hook To consume a workflow hook, create a file under `src/workflows/hooks`: ```ts title="src/workflows/hooks/product-created.ts" import { createProductsWorkflow } from "@medusajs/medusa/core-flows" createProductsWorkflow.hooks.productsCreated( async ({ products, additional_data }, { container }) => { // TODO perform an action }, async (dataFromStep, { container }) => { // undo the performed action } ) ``` This executes a custom step at the hook's designated point in the workflow. Learn more in [this documentation](!docs!/learn/fundamentals/workflows/workflow-hooks). ### Expose a Hook To expose a hook in a workflow, pass it in the second parameter of the returned `WorkflowResponse`: ```ts highlights={[["19", "hooks"]]} 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], }) } ) ``` Learn more in [this documentation](!docs!/learn/fundamentals/workflows/add-workflow-hook). ### Retry Steps To configure steps to retry in case of errors, pass the `maxRetries` step option: ```ts highlights={[["10"]]} import { createStep, } from "@medusajs/framework/workflows-sdk" export const step1 = createStep( { name: "step-1", maxRetries: 2, }, async () => { console.log("Executing step 1") throw new Error("Oops! Something happened.") } ) ``` Learn more in [this documentation](!docs!/learn/fundamentals/workflows/retry-failed-steps). ### Run Steps in Parallel If steps in a workflow don't depend on one another, run them in parallel using `parallel` from the Workflows SDK: ```ts highlights={[["22", "parallelize"]]} import { createWorkflow, WorkflowResponse, parallelize, } from "@medusajs/framework/workflows-sdk" import { createProductStep, getProductStep, createPricesStep, attachProductToSalesChannelStep, } from "./steps" interface WorkflowInput { title: string } const myWorkflow = createWorkflow( "my-workflow", (input: WorkflowInput) => { const product = createProductStep(input) const [prices, productSalesChannel] = parallelize( createPricesStep(product), attachProductToSalesChannelStep(product) ) const id = product.id const refetchedProduct = getProductStep(product.id) return new WorkflowResponse(refetchedProduct) } ) ``` Learn more in [this documentation](!docs!/learn/fundamentals/workflows/parallel-steps). ### Configure Workflow Timeout To configure the timeout of a workflow, at which the workflow's status is changed, but its execution isn't stopped, use the `timeout` configuration: ```ts highlights={[["10"]]} import { createStep, createWorkflow, WorkflowResponse, } from "@medusajs/framework/workflows-sdk" // step import... const myWorkflow = createWorkflow({ name: "hello-world", timeout: 2, // 2 seconds }, function () { const str1 = step1() return new WorkflowResponse({ message: str1, }) }) export default myWorkflow ``` Learn more in [this documentation](!docs!/learn/fundamentals/workflows/workflow-timeout). ### Configure Step Timeout To configure a step's timeout, at which its state changes but its execution isn't stopped, use the `timeout` property: ```ts highlights={[["4"]]} const step1 = createStep( { name: "step-1", timeout: 2, // 2 seconds }, async () => { // ... } ) ``` Learn more in [this documentation](!docs!/learn/fundamentals/workflows/workflow-timeout#configure-step-timeout). ### Long-Running Workflow A long-running workflow is a workflow that runs in the background. You can wait before executing some of its steps until another external or separate action occurs. To create a long-running workflow, configure any of its steps to be `async` without returning any data: ```ts highlights={[["4"]]} const step2 = createStep( { name: "step-2", async: true, }, async () => { console.log("Waiting to be successful...") } ) ``` Learn more in [this documentation](!docs!/learn/fundamentals/workflows/long-running-workflow). ### Change Step Status in Long-Running Workflow To change a step's status: 1. Grab the workflow's transaction ID when you run it: ```ts const { transaction } = await myLongRunningWorkflow(req.scope) .run() ``` 2. In an API route, workflow, or other resource, change a step's status to successful using the [Workflow Engine Module](../infrastructure-modules/workflow-engine/page.mdx): export const stepSuccessHighlights = [ ["5", "setStepSuccess", "Change a step's status to success"], ["8", "transactionId", "Pass the workflow's transaction ID"], ["9", "stepId", "The ID of the step to change its status."], ["10", "workflowId", "The ID of the workflow that the step belongs to."] ] ```ts highlights={stepSuccessHighlights} const workflowEngineService = container.resolve( Modules.WORKFLOW_ENGINE ) await workflowEngineService.setStepSuccess({ idempotencyKey: { action: TransactionHandlerType.INVOKE, transactionId, stepId: "step-2", workflowId: "hello-world", }, stepResponse: new StepResponse("Done!"), options: { container, }, }) ``` 3. In an API route, workflow, or other resource, change a step's status to failure using the [Worfklow Engine Module](../infrastructure-modules/workflow-engine/page.mdx): export const stepFailureHighlights = [ ["5", "setStepFailure", "Change a step's status to failure"], ["8", "transactionId", "Pass the workflow's transaction ID"], ["9", "stepId", "The ID of the step to change its status."], ["10", "workflowId", "The ID of the workflow that the step belongs to."] ] ```ts highlights={stepFailureHighlights} const workflowEngineService = container.resolve( Modules.WORKFLOW_ENGINE ) await workflowEngineService.setStepFailure({ idempotencyKey: { action: TransactionHandlerType.INVOKE, transactionId, stepId: "step-2", workflowId: "hello-world", }, stepResponse: new StepResponse("Failed!"), options: { container, }, }) ``` Learn more in [this documentation](!docs!/learn/fundamentals/workflows/long-running-workflow). ### Access Long-Running Workflow's Result Use the Workflow Engine Module's `subscribe` and `unsubscribe` methods to access the status of a long-running workflow. For example, in an API route: ```ts highlights={[["18", "subscribe", "Subscribe to the workflow's status changes."]]} import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" import myWorkflow from "../../../workflows/hello-world" import { Modules } from "@medusajs/framework/utils" export async function GET(req: MedusaRequest, res: MedusaResponse) { const { transaction, result } = await myWorkflow(req.scope).run() const workflowEngineService = req.scope.resolve( Modules.WORKFLOW_ENGINE ) const subscriptionOptions = { workflowId: "hello-world", transactionId: transaction.transactionId, subscriberId: "hello-world-subscriber", } await workflowEngineService.subscribe({ ...subscriptionOptions, subscriber: async (data) => { if (data.eventType === "onFinish") { console.log("Finished execution", data.result) // unsubscribe await workflowEngineService.unsubscribe({ ...subscriptionOptions, subscriberOrId: subscriptionOptions.subscriberId, }) } else if (data.eventType === "onStepFailure") { console.log("Workflow failed", data.step) } }, }) res.send(result) } ``` Learn more in [this documentation](!docs!/learn/fundamentals/workflows/long-running-workflow#access-long-running-workflow-status-and-result). --- ## Subscribers A subscriber is a function executed whenever the event it listens to is emitted. ### Create a Subscriber To create a subscriber that listens to the `product.created` event, create the file `src/subscribers/product-created.ts` with the following content: ```ts title="src/subscribers/product-created.ts" 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", } ``` Learn more in [this documentation](!docs!/learn/fundamentals/events-and-subscribers). ### Resolve Resources in Subscriber To resolve resources from the Medusa container in a subscriber, use the `container` property of its parameter: ```ts highlights={[["6", "container"], ["8", "resolve", "Resolve the Product Module's main service."]]} import { SubscriberArgs, SubscriberConfig } from "@medusajs/framework" import { Modules } from "@medusajs/framework/utils" export default async function productCreateHandler({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { const productModuleService = container.resolve(Modules.PRODUCT) const productId = data.id const product = await productModuleService.retrieveProduct( productId ) console.log(`The product ${product.title} was created`) } export const config: SubscriberConfig = { event: `product.created`, } ``` Learn more in [this documentation](!docs!/learn/fundamentals/events-and-subscribers#resolve-resources). ### Send a Notification to Reset Password To send a notification, such as an email when a user requests to reset their password, create a subscriber at `src/subscribers/handle-reset.ts` with the following content: ```ts title="src/subscribers/handle-reset.ts" 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" 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", } ``` Learn more in [this documentation](../commerce-modules/auth/reset-password/page.mdx). ### Execute a Workflow in a Subscriber To execute a workflow in a subscriber: ```ts import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" import myWorkflow from "../workflows/hello-world" import { Modules } from "@medusajs/framework/utils" export default async function handleCustomerCreate({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { const userId = data.id const userModuleService = container.resolve( Modules.USER ) const user = await userModuleService.retrieveUser(userId) const { result } = await myWorkflow(container) .run({ input: { name: user.first_name, }, }) console.log(result) } export const config: SubscriberConfig = { event: "user.created", } ``` Learn more in [this documentation](!docs!/learn/fundamentals/workflows#3-execute-the-workflow) --- ## Scheduled Jobs A scheduled job is a function executed at a specified interval of time in the background of your Medusa application. ### Create a Scheduled Job To create a scheduled job, create the file `src/jobs/hello-world.ts` with the following content: ```ts title="src/jobs/hello-world.ts" // the scheduled-job function export default function () { console.log("Time to say hello world!") } // the job's configurations export const config = { name: "every-minute-message", // execute every minute schedule: "* * * * *", } ``` Learn more in [this documentation](!docs!/learn/fundamentals/scheduled-jobs). ### Resolve Resources in Scheduled Job To resolve resources in a scheduled job, use the `container` accepted as a first parameter: ```ts highlights={[["5", "container"], ["7", "resolve", "Resolve the Product Module's main service."]]} import { MedusaContainer } from "@medusajs/framework/types" import { Modules } from "@medusajs/framework/utils" export default async function myCustomJob( container: MedusaContainer ) { const productModuleService = container.resolve(Modules.PRODUCT) const [, count] = await productModuleService.listAndCountProducts() console.log( `Time to check products! You have ${count} product(s)` ) } export const config = { name: "every-minute-message", // execute every minute schedule: "* * * * *", } ``` Learn more in [this documentation](!docs!/learn/fundamentals/scheduled-jobs#resolve-resources) ### Specify a Job's Execution Number To limit the scheduled job's execution to a number of times during the Medusa application's runtime, use the `numberOfExecutions` configuration: ```ts highlights={[["9", "numberOfExecutions"]]} 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, } ``` Learn more in [this documentation](!docs!/learn/fundamentals/scheduled-jobs/execution-number). ### Execute a Workflow in a Scheduled Job To execute a workflow in a scheduled job: ```ts import { MedusaContainer } from "@medusajs/framework/types" import myWorkflow from "../workflows/hello-world" export default async function myCustomJob( container: MedusaContainer ) { const { result } = await myWorkflow(container) .run({ input: { name: "John", }, }) console.log(result.message) } export const config = { name: "run-once-a-day", schedule: `0 0 * * *`, } ``` Learn more in [this documentation](!docs!/learn/fundamentals/workflows#3-execute-the-workflow) --- ## Loaders A loader is a function defined in a module that's executed when the Medusa application starts. ### Create a Loader To create a loader, add it to a module's `loaders` directory. For example, create the file `src/modules/hello/loaders/hello-world.ts` with the following content: ```ts title="src/modules/hello/loaders/hello-world.ts" export default async function helloWorldLoader() { console.log( "[HELLO MODULE] Just started the Medusa application!" ) } ``` Learn more in [this documentation](!docs!/learn/fundamentals/modules/loaders). ### Resolve Resources in Loader To resolve resources in a loader, use the `container` property of its first parameter: ```ts highlights={[["9", "container"], ["11", "resolve", "Resolve the Logger from the module's container."]]} import { LoaderOptions, } from "@medusajs/framework/types" import { ContainerRegistrationKeys, } from "@medusajs/framework/utils" export default async function helloWorldLoader({ container, }: LoaderOptions) { const logger = container.resolve(ContainerRegistrationKeys.LOGGER) logger.info("[helloWorldLoader]: Hello, World!") } ``` Learn more in [this documentation](!docs!/learn/fundamentals/modules/container). ### Access Module Options To access a module's options in its loader, use the `options` property of its first parameter: ```ts highlights={[["11", "options"]]} import { LoaderOptions, } from "@medusajs/framework/types" // recommended to define type in another file type ModuleOptions = { apiKey?: boolean } export default async function helloWorldLoader({ options, }: LoaderOptions) { console.log( "[HELLO MODULE] Just started the Medusa application!", options ) } ``` Learn more in [this documentation](!docs!/learn/fundamentals/modules/options). ### Register Resources in the Module's Container To register a resource in the Module's container using a loader, use the `container`'s `registerAdd` method: ```ts highlights={[["9", "registerAdd"]]} import { LoaderOptions, } from "@medusajs/framework/types" import { asValue } from "@medusajs/framework/awilix" export default async function helloWorldLoader({ container, }: LoaderOptions) { container.registerAdd( "custom_data", asValue({ test: true, }) ) } ``` Where the first parameter of `registerAdd` is the name to register the resource under, and the second parameter is the resource to register. --- ## Admin Customizations You can customize the Medusa Admin to inject widgets in existing pages, or create new pages using UI routes. For a list of components to use in the admin dashboard, refer to [this documentation](../admin-components/page.mdx). ### Create Widget A widget is a React component that can be injected into an existing page in the admin dashboard. To create a widget in the admin dashboard, create the file `src/admin/widgets/products-widget.tsx` with the following content: ```tsx title="src/admin/widgets/products-widget.tsx" import { defineWidgetConfig } from "@medusajs/admin-sdk" import { Container, Heading } from "@medusajs/ui" const ProductWidget = () => { return (
Product Widget
) } export const config = defineWidgetConfig({ zone: "product.list.before", }) export default ProductWidget ``` Learn more about widgets in [this documentation](!docs!/learn/fundamentals/admin/widgets). ### Receive Details Props in Widgets Widgets created in a details page, such as widgets in the `product.details.before` injection zone, receive a prop of the data of the details page (for example, the product): ```tsx highlights={[["10", "data"]]} import { defineWidgetConfig } from "@medusajs/admin-sdk" import { Container, Heading } from "@medusajs/ui" import { DetailWidgetProps, AdminProduct, } from "@medusajs/framework/types" // The widget const ProductWidget = ({ data, }: DetailWidgetProps) => { return (
Product Widget {data.title}
) } // The widget's configurations export const config = defineWidgetConfig({ zone: "product.details.before", }) export default ProductWidget ``` Learn more in [this documentation](!docs!/learn/fundamentals/admin/widgets#detail-widget-props). ### Create a UI Route A UI route is a React Component that adds a new page to your admin dashboard. The UI Route can be shown in the sidebar or added as a nested page. To create a UI route in the admin dashboard, create the file `src/admin/routes/custom/page.tsx` with the following content: ```tsx title="src/admin/routes/custom/page.tsx" import { defineRouteConfig } from "@medusajs/admin-sdk" import { ChatBubbleLeftRight } from "@medusajs/icons" import { Container, Heading } from "@medusajs/ui" const CustomPage = () => { return (
This is my custom route
) } export const config = defineRouteConfig({ label: "Custom Route", icon: ChatBubbleLeftRight, }) export default CustomPage ``` This adds a new page at `localhost:9000/app/custom`. Learn more in [this documentation](!docs!/learn/fundamentals/admin/ui-routes). ### Create Settings Page To create a settings page, create a UI route under the `src/admin/routes/settings` directory. For example, create the file `src/admin/routes/settings/custom/page.tsx` with the following content: ```tsx title="src/admin/routes/settings/custom/page.tsx" import { defineRouteConfig } from "@medusajs/admin-sdk" import { Container, Heading } from "@medusajs/ui" const CustomSettingPage = () => { return (
Custom Setting Page
) } export const config = defineRouteConfig({ label: "Custom", }) export default CustomSettingPage ``` This adds a setting page at `localhost:9000/app/settings/custom`. Learn more in [this documentation](!docs!/learn/fundamentals/admin/ui-routes#create-settings-page) ### Accept Path Parameters in UI Routes To accept a path parameter in a UI route, name one of the directories in its path in the format `[param]`. For example, create the file `src/admin/routes/custom/[id]/page.tsx` with the following content: ```tsx title="src/admin/routes/custom/[id]/page.tsx" import { useParams } from "react-router-dom" import { Container } from "@medusajs/ui" const CustomPage = () => { const { id } = useParams() return (
Passed ID: {id}
) } export default CustomPage ``` This creates a UI route at `localhost:9000/app/custom/:id`, where `:id` is a path parameter. Learn more in [this documentation](!docs!/learn/fundamentals/admin/ui-routes#path-parameters) ### Send Request to API Route To send a request to custom API routes from the admin dashboard, use the Fetch API. For example: ```tsx import { defineWidgetConfig } from "@medusajs/admin-sdk" import { Container } from "@medusajs/ui" import { useEffect, useState } from "react" const ProductWidget = () => { const [productsCount, setProductsCount] = useState(0) const [loading, setLoading] = useState(true) useEffect(() => { if (!loading) { return } fetch(`/admin/products`, { credentials: "include", }) .then((res) => res.json()) .then(({ count }) => { setProductsCount(count) setLoading(false) }) }, [loading]) return ( {loading && Loading...} {!loading && You have {productsCount} Product(s).} ) } export const config = defineWidgetConfig({ zone: "product.list.before", }) export default ProductWidget ``` Learn more in [this documentation](!docs!/learn/fundamentals/admin/tips#send-requests-to-api-routes) ### Add Link to Another Page To add a link to another page in a UI route or a widget, use `react-router-dom`'s `Link` component: ```tsx import { defineWidgetConfig } from "@medusajs/admin-sdk" import { Container } from "@medusajs/ui" import { Link } from "react-router-dom" // The widget const ProductWidget = () => { return ( View Orders ) } // The widget's configurations export const config = defineWidgetConfig({ zone: "product.details.before", }) export default ProductWidget ``` Learn more in [this documentation](!docs!/learn/fundamentals/admin/tips#routing-functionalities). --- ## Integration Tests Medusa provides a `@medusajs/test-utils` package with utility tools to create integration tests for your custom API routes, modules, or other Medusa customizations. For details on setting up your project for integration tests, refer to [this documentation](!docs!/learn/debugging-and-testing/testing-tools). ### Test Custom API Route To create a test for a custom API route, create the file `integration-tests/http/custom-routes.spec.ts` with the following content: ```ts title="integration-tests/http/custom-routes.spec.ts" import { medusaIntegrationTestRunner } from "@medusajs/test-utils" medusaIntegrationTestRunner({ testSuite: ({ api, getContainer }) => { describe("Custom endpoints", () => { describe("GET /custom", () => { it("returns correct message", async () => { const response = await api.get( `/custom` ) expect(response.status).toEqual(200) expect(response.data).toHaveProperty("message") expect(response.data.message).toEqual("Hello, World!") }) }) }) }, }) ``` Then, run the test with the following command: ```bash npm2yarn npm run test:integration ``` Learn more in [this documentation](!docs!/learn/debugging-and-testing/testing-tools/integration-tests/api-routes). ### Test Workflow To create a test for a workflow, create the file `integration-tests/http/workflow.spec.ts` with the following content: ```ts title="integration-tests/http/workflow.spec.ts" import { medusaIntegrationTestRunner } from "@medusajs/test-utils" import { helloWorldWorkflow } from "../../src/workflows/hello-world" medusaIntegrationTestRunner({ testSuite: ({ getContainer }) => { describe("Test hello-world workflow", () => { it("returns message", async () => { const { result } = await helloWorldWorkflow(getContainer()) .run() expect(result).toEqual("Hello, World!") }) }) }, }) ``` Then, run the test with the following command: ```bash npm2yarn npm run test:integration ``` Learn more in [this documentation](!docs!/learn/debugging-and-testing/testing-tools/integration-tests/workflows). ### Test Module's Service To create a test for a module's service, create the test under the `__tests__` directory of the module. For example, 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 Post from "../models/post" moduleIntegrationTestRunner({ moduleName: BLOG_MODULE, moduleModels: [Post], resolve: "./modules/blog", testSuite: ({ service }) => { describe("BlogModuleService", () => { it("says hello world", () => { const message = service.getMessage() expect(message).toEqual("Hello, World!") }) }) }, }) ``` Then, run the test with the following command: ```bash npm2yarn npm run test:modules ``` --- ## Commerce Modules Medusa provides all its commerce features as separate Commerce Modules, such as the Product or Order modules. Refer to the [Commerce Modules](../commerce-modules/page.mdx) documentation for concepts and reference of every module's main service. ### Create an Actor Type to Authenticate To create an actor type that can authenticate to the Medusa application, such as a `manager`: 1. Create the data model in a module: ```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 ``` 2. Use the `setAuthAppMetadataStep` as a step in a workflow that creates a manager: ```ts import { createWorkflow, WorkflowResponse, } from "@medusajs/framework/workflows-sdk" import { setAuthAppMetadataStep, } from "@medusajs/medusa/core-flows" // other imports... 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) } ) ``` 3. Use the workflow in an API route that creates a user (manager) of the actor type: ```ts 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 }) } ``` 4. Apply the `authenticate` middleware on the new route in `src/api/middlewares.ts`: ```ts title="src/api/middlewares.ts" 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"]), ], }, ], }) ``` Now, manager users can use the `/manager` API route to register, and all routes starting with `/manager/me` are only accessible by authenticated managers. Find an elaborate example and learn more in [this documentation](../commerce-modules/auth/create-actor-type/page.mdx). ### Apply Promotion on Cart Items and Shipping To apply a promotion on a cart's items and shipping methods using the [Cart](../commerce-modules/cart/page.mdx) and [Promotion](../commerce-modules/promotion/page.mdx) modules: ```ts import { ComputeActionAdjustmentLine, ComputeActionItemLine, ComputeActionShippingLine, AddItemAdjustmentAction, AddShippingMethodAdjustment, // ... } from "@medusajs/framework/types" // retrieve the cart const cart = await cartModuleService.retrieveCart("cart_123", { relations: [ "items.adjustments", "shipping_methods.adjustments", ], }) // retrieve line item adjustments const lineItemAdjustments: ComputeActionItemLine[] = [] cart.items.forEach((item) => { const filteredAdjustments = item.adjustments?.filter( (adjustment) => adjustment.code !== undefined ) as unknown as ComputeActionAdjustmentLine[] if (filteredAdjustments.length) { lineItemAdjustments.push({ ...item, adjustments: filteredAdjustments, }) } }) // retrieve shipping method adjustments const shippingMethodAdjustments: ComputeActionShippingLine[] = [] cart.shipping_methods.forEach((shippingMethod) => { const filteredAdjustments = shippingMethod.adjustments?.filter( (adjustment) => adjustment.code !== undefined ) as unknown as ComputeActionAdjustmentLine[] if (filteredAdjustments.length) { shippingMethodAdjustments.push({ ...shippingMethod, adjustments: filteredAdjustments, }) } }) // compute actions const actions = await promotionModuleService.computeActions( ["promo_123"], { items: lineItemAdjustments, shipping_methods: shippingMethodAdjustments, } ) // set the adjustments on the line item await cartModuleService.setLineItemAdjustments( cart.id, actions.filter( (action) => action.action === "addItemAdjustment" ) as AddItemAdjustmentAction[] ) // set the adjustments on the shipping method await cartModuleService.setShippingMethodAdjustments( cart.id, actions.filter( (action) => action.action === "addShippingMethodAdjustment" ) as AddShippingMethodAdjustment[] ) ``` Learn more in [this documentation](../commerce-modules/cart/tax-lines/page.mdx). ### Retrieve Tax Lines of a Cart's Items and Shipping To retrieve the tax lines of a cart's items and shipping methods using the [Cart](../commerce-modules/cart/page.mdx) and [Tax](../commerce-modules/tax/page.mdx) modules: ```ts // retrieve the cart const cart = await cartModuleService.retrieveCart("cart_123", { relations: [ "items.tax_lines", "shipping_methods.tax_lines", "shipping_address", ], }) // retrieve the tax lines const taxLines = await taxModuleService.getTaxLines( [ ...(cart.items as TaxableItemDTO[]), ...(cart.shipping_methods as TaxableShippingDTO[]), ], { address: { ...cart.shipping_address, country_code: cart.shipping_address.country_code || "us", }, } ) // set line item tax lines await cartModuleService.setLineItemTaxLines( cart.id, taxLines.filter((line) => "line_item_id" in line) ) // set shipping method tax lines await cartModuleService.setLineItemTaxLines( cart.id, taxLines.filter((line) => "shipping_line_id" in line) ) ``` Learn more in [this documentation](../commerce-modules/cart/tax-lines/page.mdx) ### Apply Promotion on an Order's Items and Shipping To apply a promotion on an order's items and shipping methods using the [Order](../commerce-modules/order/page.mdx) and [Promotion](../commerce-modules/promotion/page.mdx) modules: ```ts import { ComputeActionAdjustmentLine, ComputeActionItemLine, ComputeActionShippingLine, AddItemAdjustmentAction, AddShippingMethodAdjustment, // ... } from "@medusajs/framework/types" // ... // retrieve the order const order = await orderModuleService.retrieveOrder("ord_123", { relations: [ "items.item.adjustments", "shipping_methods.shipping_method.adjustments", ], }) // retrieve the line item adjustments const lineItemAdjustments: ComputeActionItemLine[] = [] order.items.forEach((item) => { const filteredAdjustments = item.adjustments?.filter( (adjustment) => adjustment.code !== undefined ) as unknown as ComputeActionAdjustmentLine[] if (filteredAdjustments.length) { lineItemAdjustments.push({ ...item, ...item.detail, adjustments: filteredAdjustments, }) } }) //retrieve shipping method adjustments const shippingMethodAdjustments: ComputeActionShippingLine[] = [] order.shipping_methods.forEach((shippingMethod) => { const filteredAdjustments = shippingMethod.adjustments?.filter( (adjustment) => adjustment.code !== undefined ) as unknown as ComputeActionAdjustmentLine[] if (filteredAdjustments.length) { shippingMethodAdjustments.push({ ...shippingMethod, adjustments: filteredAdjustments, }) } }) // compute actions const actions = await promotionModuleService.computeActions( ["promo_123"], { items: lineItemAdjustments, shipping_methods: shippingMethodAdjustments, // TODO infer from cart or region currency_code: "usd", } ) // set the adjustments on the line items await orderModuleService.setOrderLineItemAdjustments( order.id, actions.filter( (action) => action.action === "addItemAdjustment" ) as AddItemAdjustmentAction[] ) // set the adjustments on the shipping methods await orderModuleService.setOrderShippingMethodAdjustments( order.id, actions.filter( (action) => action.action === "addShippingMethodAdjustment" ) as AddShippingMethodAdjustment[] ) ``` Learn more in [this documentation](../commerce-modules/order/promotion-adjustments/page.mdx) ### Accept Payment using Module To accept payment using the Payment Module's main service: 1. Create a payment collection and link it to the cart: ```ts import { ContainerRegistrationKeys, Modules, } from "@medusajs/framework/utils" // ... const paymentCollection = await paymentModuleService.createPaymentCollections({ region_id: "reg_123", currency_code: "usd", amount: 5000, }) // resolve Link const link = container.resolve( ContainerRegistrationKeys.LINK ) // create a link between the cart and payment collection link.create({ [Modules.CART]: { cart_id: "cart_123", }, [Modules.PAYMENT]: { payment_collection_id: paymentCollection.id, }, }) ``` 2. Create a payment session in the collection: ```ts const paymentSession = await paymentModuleService.createPaymentSession( paymentCollection.id, { provider_id: "stripe", currency_code: "usd", amount: 5000, data: { // any necessary data for the // payment provider }, } ) ``` 3. Authorize the payment session: ```ts const payment = await paymentModuleService.authorizePaymentSession( paymentSession.id, {} ) ``` Learn more in [this documentation](../commerce-modules/payment/payment-flow/page.mdx). ### Get Variant's Prices for Region and Currency To get prices of a product variant for a region and currency using [Query](!docs!/learn/fundamentals/module-links/query): ```ts 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", }), }, }, }) ``` Learn more in [this documentation](../commerce-modules/product/guides/price/page.mdx#retrieve-calculated-price-for-a-context). ### Get All Variant's Prices To get all prices of a product variant using [Query](!docs!/learn/fundamentals/module-links/query): ```ts const { data: products } = await query.graph({ entity: "product", fields: [ "*", "variants.*", "variants.prices.*", ], filters: { id: [ "prod_123", ], }, }) ``` Learn more in [this documentation](../commerce-modules/product/guides/price/page.mdx). ### Get Variant Prices with Taxes To get a variant's prices with taxes using [Query](!docs!/learn/fundamentals/module-links/query) and the [Tax Module](../commerce-modules/tax/page.mdx) ```ts import { HttpTypes, TaxableItemDTO, ItemTaxLineDTO, } from "@medusajs/framework/types" import { QueryContext, calculateAmountsWithTax, } from "@medusajs/framework/utils" // other imports... // ... const asTaxItem = (product: HttpTypes.StoreProduct): TaxableItemDTO[] => { return product.variants ?.map((variant) => { if (!variant.calculated_price) { return } return { id: variant.id, product_id: product.id, product_name: product.title, product_categories: product.categories?.map((c) => c.name), product_category_id: product.categories?.[0]?.id, product_sku: variant.sku, product_type: product.type, product_type_id: product.type_id, quantity: 1, unit_price: variant.calculated_price.calculated_amount, currency_code: variant.calculated_price.currency_code, } }) .filter((v) => !!v) as unknown as TaxableItemDTO[] } const { data: products } = await query.graph({ entity: "product", fields: [ "*", "variants.*", "variants.calculated_price.*", ], filters: { id: "prod_123", }, context: { variants: { calculated_price: QueryContext({ region_id: "region_123", currency_code: "usd", }), }, }, }) const taxLines = (await taxModuleService.getTaxLines( products.map(asTaxItem).flat(), { // example of context properties. You can pass other ones. address: { country_code, }, } )) as unknown as ItemTaxLineDTO[] const taxLinesMap = new Map() taxLines.forEach((taxLine) => { const variantId = taxLine.line_item_id if (!taxLinesMap.has(variantId)) { taxLinesMap.set(variantId, []) } taxLinesMap.get(variantId)?.push(taxLine) }) products.forEach((product) => { product.variants?.forEach((variant) => { if (!variant.calculated_price) { return } const taxLinesForVariant = taxLinesMap.get(variant.id) || [] const { priceWithTax, priceWithoutTax } = calculateAmountsWithTax({ taxLines: taxLinesForVariant, amount: variant.calculated_price!.calculated_amount!, includesTax: variant.calculated_price!.is_calculated_price_tax_inclusive!, }) // do something with prices... }) }) ``` Learn more in [this documentation](../commerce-modules/product/guides/price-with-taxes/page.mdx). ### Invite Users To invite a user using the [User Module](../commerce-modules/user/page.mdx): ```ts const invite = await userModuleService.createInvites({ email: "user@example.com", }) ``` ### Accept User Invite To accept an invite and create a user using the [User Module](../commerce-modules/user/page.mdx): ```ts const invite = await userModuleService.validateInviteToken(inviteToken) await userModuleService.updateInvites({ id: invite.id, accepted: true, }) const user = await userModuleService.createUsers({ email: invite.email, }) ```