on GitHub" data-tooltip-id=":Rblcldb:">v2.6·
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.
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 the file src/api/hello-world/route.ts
with the following content:
This creates a GET
API route at /hello-world
.
Learn more in this documentation.
To resolve resources from the Medusa container in an API route:
1import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"2import { Modules } from "@medusajs/framework/utils"3 4export const GET = async (5 req: MedusaRequest, 6 res: MedusaResponse7) => {8 const productModuleService = req.scope.resolve(9 Modules.PRODUCT10 )11 12 const [, count] = await productModuleService13 .listAndCountProducts()14 15 res.json({16 count,17 })18}
This resolves the Product Module's main service.
Learn more in this documentation.
API routes can accept path parameters.
To do that, create the file src/api/hello-world/[id]/route.ts
with the following content:
Learn more about path parameters in this documentation.
API routes can accept query parameters:
Learn more about query parameters in this documentation.
API routes can accept request body parameters:
Learn more about request body parameters in this documentation.
You can change the response code of an API route:
Learn more about setting the response code in this documentation.
To execute a workflow in an API route:
1import type {2 MedusaRequest,3 MedusaResponse,4} from "@medusajs/framework/http"5import myWorkflow from "../../workflows/hello-world"6 7export async function GET(8 req: MedusaRequest,9 res: MedusaResponse10) {11 const { result } = await myWorkflow(req.scope)12 .run({13 input: {14 name: req.query.name as string,15 },16 })17 18 res.send(result)19}
Learn more in this documentation.
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
:
1import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"2 3export const GET = async (4 req: MedusaRequest,5 res: MedusaResponse6) => {7 res.writeHead(200, {8 "Content-Type": "text/event-stream",9 "Cache-Control": "no-cache",10 Connection: "keep-alive",11 })12 13 const interval = setInterval(() => {14 res.write("Streaming data...\n")15 }, 3000)16 17 req.on("end", () => {18 clearInterval(interval)19 res.end()20 })21}
This changes the response type to return an event stream.
Learn more in this documentation.
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:
1import type { 2 MedusaNextFunction, 3 MedusaRequest, 4 MedusaResponse, 5 defineMiddlewares,6} from "@medusajs/framework/http"7 8export default defineMiddlewares({9 routes: [10 {11 matcher: "/custom*",12 middlewares: [13 (14 req: MedusaRequest, 15 res: MedusaResponse, 16 next: MedusaNextFunction17 ) => {18 console.log("Received a request!")19 20 next()21 },22 ],23 },24 {25 matcher: "/custom/:id",26 middlewares: [27 (28 req: MedusaRequest, 29 res: MedusaResponse, 30 next: MedusaNextFunction31 ) => {32 console.log("With Path Parameter")33 34 next()35 },36 ],37 },38 ],39})
Learn more about middlewares in this documentation.
To restrict a middleware to an HTTP method:
src/api/custom/validators.ts
:src/api/middlewares.ts
:1import { 2 validateAndTransformBody,3 defineMiddlewares,4} from "@medusajs/framework/http"5import { PostStoreCustomSchema } from "./custom/validators"6 7export default defineMiddlewares({8 routes: [9 {10 matcher: "/custom",11 method: "POST",12 middlewares: [13 validateAndTransformBody(PostStoreCustomSchema),14 ],15 },16 ],17})
/custom
API route:1import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"2import { z } from "zod"3import { PostStoreCustomSchema } from "./validators"4 5type PostStoreCustomSchemaType = z.infer<6 typeof PostStoreCustomSchema7>8 9export const POST = async (10 req: MedusaRequest<PostStoreCustomSchemaType>,11 res: MedusaResponse12) => {13 res.json({14 sum: req.validatedBody.a + req.validatedBody.b,15 })16}
Learn more about request body validation in this documentation.
In this example, you'll pass additional data to the Create Product API route, then consume its hook:
src/api/middlewares.ts
with the following content:src/workflows/hooks/created-product.ts
with the following content:1import { createProductsWorkflow } from "@medusajs/medusa/core-flows"2import { StepResponse } from "@medusajs/framework/workflows-sdk"3 4createProductsWorkflow.hooks.productsCreated(5 (async ({ products, additional_data }, { container }) => {6 if (!additional_data.brand_id) {7 return new StepResponse([], [])8 }9 10 // TODO perform custom action11 }),12 (async (links, { container }) => {13 // TODO undo the action in the compensation14 })15 16)
You can protect API routes by restricting access to authenticated admin users only.
Add the following middleware in src/api/middlewares.ts
:
Learn more in this documentation.
You can protect API routes by restricting access to authenticated customers only.
Add the following middleware in src/api/middlewares.ts
:
Learn more in this documentation.
To retrieve the currently logged-in user in an API route:
1import type {2 AuthenticatedMedusaRequest,3 MedusaResponse,4} from "@medusajs/framework/http"5import { Modules } from "@medusajs/framework/utils"6 7export const GET = async (8 req: AuthenticatedMedusaRequest,9 res: MedusaResponse10) => {11 const userModuleService = req.scope.resolve(12 Modules.USER13 )14 15 const user = await userModuleService.retrieveUser(16 req.auth_context.actor_id17 )18 19 // ...20}
Learn more in this documentation.
To retrieve the currently logged-in customer in an API route:
1import type {2 AuthenticatedMedusaRequest,3 MedusaResponse,4} from "@medusajs/framework/http"5import { Modules } from "@medusajs/framework/utils"6 7export const GET = async (8 req: AuthenticatedMedusaRequest,9 res: MedusaResponse10) => {11 if (req.auth_context?.actor_id) {12 // retrieve customer13 const customerModuleService = req.scope.resolve(14 Modules.CUSTOMER15 )16 17 const customer = await customerModuleService.retrieveCustomer(18 req.auth_context.actor_id19 )20 }21 22 // ...23}
Learn more in this documentation.
To throw errors in an API route, use MedusaError
from the Medusa Framework:
1import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"2import { MedusaError } from "@medusajs/framework/utils"3 4export const GET = async (5 req: MedusaRequest,6 res: MedusaResponse7) => {8 if (!req.query.q) {9 throw new MedusaError(10 MedusaError.Types.INVALID_DATA,11 "The `q` query parameter is required."12 )13 }14 15 // ...16}
Learn more in this documentation.
To override the error handler of API routes, create the file src/api/middlewares.ts
with the following content:
1import { 2 defineMiddlewares, 3 MedusaNextFunction, 4 MedusaRequest, 5 MedusaResponse,6} from "@medusajs/framework/http"7import { MedusaError } from "@medusajs/framework/utils"8 9export default defineMiddlewares({10 errorHandler: (11 error: MedusaError | any, 12 req: MedusaRequest, 13 res: MedusaResponse, 14 next: MedusaNextFunction15 ) => {16 res.status(400).json({17 error: "Something happened.",18 })19 },20})
Learn more in this documentation,
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:
1import type { 2 MedusaNextFunction, 3 MedusaRequest, 4 MedusaResponse, 5 defineMiddlewares,6} from "@medusajs/framework/http"7import { ConfigModule } from "@medusajs/framework/types"8import { parseCorsOrigins } from "@medusajs/framework/utils"9import cors from "cors"10 11export default defineMiddlewares({12 routes: [13 {14 matcher: "/custom*",15 middlewares: [16 (17 req: MedusaRequest, 18 res: MedusaResponse, 19 next: MedusaNextFunction20 ) => {21 const configModule: ConfigModule =22 req.scope.resolve("configModule")23 24 return cors({25 origin: parseCorsOrigins(26 configModule.projectConfig.http.storeCors27 ),28 credentials: true,29 })(req, res, next)30 },31 ],32 },33 ],34})
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:
To access the raw body data in your route, use the req.rawBody
property:
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.
src/modules/hello
.src/modules/hello/models/my-custom.ts
with the following data model:src/modules/hello/service.ts
with the following service:src/modules/hello/index.ts
that exports the module definition:medusa-config.ts
:1import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"2import HelloModuleService from "../../modules/hello/service"3import { HELLO_MODULE } from "../../modules/hello"4 5export async function GET(6 req: MedusaRequest,7 res: MedusaResponse8): Promise<void> {9 const helloModuleService: HelloModuleService = req.scope.resolve(10 HELLO_MODULE11 )12 13 const my_custom = await helloModuleService.createMyCustoms({14 name: "test",15 })16 17 res.json({18 my_custom,19 })20}
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/hello/services/custom.ts
with the following content:
Then, export the service in the file src/modules/hello/services/index.ts
:
Finally, resolve the service in your module's main service or loader:
1import { MedusaService } from "@medusajs/framework/utils"2import MyCustom from "./models/my-custom"3import { CustomService } from "./services"4 5type InjectedDependencies = {6 customService: CustomService7}8 9class HelloModuleService extends MedusaService({10 MyCustom,11}){12 private customService: CustomService13 14 constructor({ customService }: InjectedDependencies) {15 super(...arguments)16 17 this.customService = customService18 }19}20 21export default HelloModuleService
Learn more in this documentation.
A module can accept options for configurations and secrets.
To accept options in your module:
medusa-config.ts
:1import { MedusaService } from "@medusajs/framework/utils"2import MyCustom from "./models/my-custom"3 4// recommended to define type in another file5type ModuleOptions = {6 apiKey?: boolean7}8 9export default class HelloModuleService extends MedusaService({10 MyCustom,11}){12 protected options_: ModuleOptions13 14 constructor({}, options?: ModuleOptions) {15 super(...arguments)16 17 this.options_ = options || {18 apiKey: false,19 }20 }21 22 // ...23}
Learn more in this documentation.
An example of integrating a dummy third-party system in a module's service:
1import { Logger } from "@medusajs/framework/types"2import { BRAND_MODULE } from ".."3 4export type ModuleOptions = {5 apiKey: string6}7 8type InjectedDependencies = {9 logger: Logger10}11 12export class BrandClient {13 private options_: ModuleOptions14 private logger_: Logger15 16 constructor(17 { logger }: InjectedDependencies, 18 options: ModuleOptions19 ) {20 this.logger_ = logger21 this.options_ = options22 }23 24 private async sendRequest(url: string, method: string, data?: any) {25 this.logger_.info(`Sending a ${26 method27 } request to ${url}. data: ${JSON.stringify(data, null, 2)}`)28 this.logger_.info(`Client Options: ${29 JSON.stringify(this.options_, null, 2)30 }`)31 }32}
Find a longer example of integrating a third-party service in this documentation.
A data model represents a table in the database. Medusa provides a data model language to intuitively create data models.
To create a data model in a module:
src/modules/hello/models/my-custom.ts
with the following data model:Learn more in this documentation.
A data model can have properties of the following types:
Learn more in this documentation.
To set an id
property as the primary key of a data model:
To set a text
property as the primary key:
To set a number
property as the primary key:
Learn more in this documentation.
To set the default value of a property:
Learn more in this documentation.
To allow null
values for a property:
Learn more in this documentation.
To create a unique index on a property:
Learn more in this documentation.
To define a database index on a property:
Learn more in this documentation.
To define a composite index on a data model:
1import { model } from "@medusajs/framework/utils"2 3const MyCustom = model.define("my_custom", {4 id: model.id().primaryKey(),5 name: model.text(),6 age: model.number().nullable(),7}).indexes([8 {9 on: ["name", "age"],10 where: {11 age: {12 $ne: null,13 },14 },15 },16])17 18export default MyCustom
Learn more in this documentation.
To make a property searchable using terms or keywords:
Then, to search by that property, pass the q
filter to the list
or listAndCount
generated methods of the module's main service:
helloModuleService
is the main service that the data models belong to.Learn more in this documentation.
The following creates a one-to-one relationship between the User
and Email
data models:
Learn more in this documentation.
The following creates a one-to-many relationship between the Store
and Product
data models:
1import { model } from "@medusajs/framework/utils"2 3const Store = model.define("store", {4 id: model.id().primaryKey(),5 products: model.hasMany(() => Product),6})7 8const Product = model.define("product", {9 id: model.id().primaryKey(),10 store: model.belongsTo(() => Store, {11 mappedBy: "products",12 }),13})
Learn more in this documentation.
The following creates a many-to-many relationship between the Order
and Product
data models:
1import { model } from "@medusajs/framework/utils"2 3const Order = model.define("order", {4 id: model.id().primaryKey(),5 products: model.manyToMany(() => Product, {6 mappedBy: "orders",7 }),8})9 10const Product = model.define("product", {11 id: model.id().primaryKey(),12 orders: model.manyToMany(() => Order, {13 mappedBy: "products",14 }),15})
Learn more in this documentation.
To configure cascade on a data model:
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.
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:
helloModuleService
is the main service that the data models belong to.And to set the ID of a user's email when creating or updating it:
Learn more in this documentation.
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:
helloModuleService
is the main service that the data models belong to.Learn more in this documentation
Consider you have a many-to-many relationship between Order
and Product
data models.
To set the orders a product has when creating it:
helloModuleService
is the main service that the data models belong to.To add new orders to a product without removing the previous associations:
Learn more in this documentation.
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:
helloModuleService
is the main service that the data models belong to.Learn more in this documentation.
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.
The service factory MedusaService
generates data-management methods for your data models.
To extend the service factory in your module's service:
The HelloModuleService
will now have data-management methods for MyCustom
.
Refer to this reference for details on the generated methods.
Learn more about the service factory in this documentation.
To resolve resources from the module's container in a service:
Learn more in this documentation.
To access options passed to a module in its service:
1import { MedusaService } from "@medusajs/framework/utils"2import MyCustom from "./models/my-custom"3 4// recommended to define type in another file5type ModuleOptions = {6 apiKey?: boolean7}8 9export default class HelloModuleService extends MedusaService({10 MyCustom,11}){12 protected options_: ModuleOptions13 14 constructor({}, options?: ModuleOptions) {15 super(...arguments)16 17 this.options_ = options || {18 apiKey: "",19 }20 }21 22 // ...23}
Learn more in this documentation.
To run database query in your service:
1// other imports...2import { 3 InjectManager,4 MedusaContext,5} from "@medusajs/framework/utils"6 7class HelloModuleService {8 // ...9 10 @InjectManager()11 async getCount(12 @MedusaContext() sharedContext?: Context<EntityManager>13 ): Promise<number> {14 return await sharedContext.manager.count("my_custom")15 }16 17 @InjectManager()18 async getCountSql(19 @MedusaContext() sharedContext?: Context<EntityManager>20 ): Promise<number> {21 const data = await sharedContext.manager.execute(22 "SELECT COUNT(*) as num FROM my_custom"23 ) 24 25 return parseInt(data[0].num)26 }27}
Learn more in this documentation
To execute database operations within a transaction in your service:
1import { 2 InjectManager,3 InjectTransactionManager,4 MedusaContext,5} from "@medusajs/framework/utils"6import { Context } from "@medusajs/framework/types"7import { EntityManager } from "@mikro-orm/knex"8 9class HelloModuleService {10 // ...11 @InjectTransactionManager()12 protected async update_(13 input: {14 id: string,15 name: string16 },17 @MedusaContext() sharedContext?: Context<EntityManager>18 ): Promise<any> {19 const transactionManager = sharedContext.transactionManager20 await transactionManager.nativeUpdate(21 "my_custom",22 {23 id: input.id,24 },25 {26 name: input.name,27 }28 )29 30 // retrieve again31 const updatedRecord = await transactionManager.execute(32 `SELECT * FROM my_custom WHERE id = '${input.id}'`33 )34 35 return updatedRecord36 }37 38 @InjectManager()39 async update(40 input: {41 id: string,42 name: string43 },44 @MedusaContext() sharedContext?: Context<EntityManager>45 ) {46 return await this.update_(input, sharedContext)47 }48}
Learn more in this documentation.
A module link forms an association between two data models of different modules, while maintaining module isolation.
To define a link between your custom module and a commerce module, such as the Product Module:
src/links/hello-product.ts
with the following content:Learn more in this documentation.
To define a list link, where multiple records of a model can be linked to a record in another:
Learn more about list links in this documentation.
To ensure a model's records linked to another model are deleted when the linked model is deleted:
Learn more in this documentation.
To add a custom column to the table that stores the linked records of two data models:
1import HelloModule from "../modules/hello"2import ProductModule from "@medusajs/medusa/product"3import { defineLink } from "@medusajs/framework/utils"4 5export default defineLink(6 ProductModule.linkable.product,7 HelloModule.linkable.myCustom,8 {9 database: {10 extraColumns: {11 metadata: {12 type: "json",13 },14 },15 },16 }17)
Then, to set the custom column when creating or updating a link between records:
To retrieve the custom column when retrieving linked records using Query:
Learn more in this documentation.
To create a link between two records using Link:
Learn more in this documentation.
To dismiss links between records using Link:
Learn more in this documentation.
To cascade delete records linked to a deleted record:
Learn more in this documentation.
To restore records that were soft-deleted because they were linked to a soft-deleted record:
Learn more in this documentation.
Query fetches data across modules. It’s a set of methods registered in the Medusa container under the query
key.
To retrieve records using Query in an API route:
1import {2 MedusaRequest,3 MedusaResponse,4} from "@medusajs/framework/http"5import {6 ContainerRegistrationKeys,7} from "@medusajs/framework/utils"8 9export const GET = async (10 req: MedusaRequest,11 res: MedusaResponse12) => {13 const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)14 15 const { data: myCustoms } = await query.graph({16 entity: "my_custom",17 fields: ["id", "name"],18 })19 20 res.json({ my_customs: myCustoms })21}
Learn more in this documentation.
To retrieve records linked to a data model:
1import {2 MedusaRequest,3 MedusaResponse,4} from "@medusajs/framework/http"5import {6 ContainerRegistrationKeys,7} from "@medusajs/framework/utils"8 9export const GET = async (10 req: MedusaRequest,11 res: MedusaResponse12) => {13 const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)14 15 const { data: myCustoms } = await query.graph({16 entity: "my_custom",17 fields: [18 "id", 19 "name",20 "product.*",21 ],22 })23 24 res.json({ my_customs: myCustoms })25}
Learn more in this documentation.
To filter the retrieved records:
1import {2 MedusaRequest,3 MedusaResponse,4} from "@medusajs/framework/http"5import {6 ContainerRegistrationKeys,7} from "@medusajs/framework/utils"8 9export const GET = async (10 req: MedusaRequest,11 res: MedusaResponse12) => {13 const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)14 15 const { data: myCustoms } = await query.graph({16 entity: "my_custom",17 fields: ["id", "name"],18 filters: {19 id: [20 "mc_01HWSVWR4D2XVPQ06DQ8X9K7AX",21 "mc_01HWSVWK3KYHKQEE6QGS2JC3FX",22 ],23 },24 })25 26 res.json({ my_customs: myCustoms })27}
Learn more in this documentation.
To paginate and sort retrieved records:
1import {2 MedusaRequest,3 MedusaResponse,4} from "@medusajs/framework/http"5import {6 ContainerRegistrationKeys,7} from "@medusajs/framework/utils"8 9export const GET = async (10 req: MedusaRequest,11 res: MedusaResponse12) => {13 const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)14 15 const { 16 data: myCustoms,17 metadata: { count, take, skip } = {},18 } = await query.graph({19 entity: "my_custom",20 fields: ["id", "name"],21 pagination: {22 skip: 0,23 take: 10,24 order: {25 name: "DESC",26 },27 },28 })29 30 res.json({ 31 my_customs: myCustoms,32 count,33 take,34 skip,35 })36}
Learn more in this documentation.
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.
To create a workflow:
src/workflows/hello-world/steps/step-1.ts
with the following content:src/workflows/hello-world/steps/step-2.ts
with the following content:1import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"2 3type StepInput = {4 name: string5}6 7export const step2 = createStep(8 "step-2", 9 async ({ name }: StepInput) => {10 return new StepResponse(`Hello ${name} from step two!`)11 }12)
src/workflows/hello-world/index.ts
with the following content:1import {2 createWorkflow,3 WorkflowResponse,4} from "@medusajs/framework/workflows-sdk"5import { step1 } from "./steps/step-1"6import { step2 } from "./steps/step-2"7 8const myWorkflow = createWorkflow(9 "hello-world",10 function (input: WorkflowInput) {11 const str1 = step1()12 // to pass input13 const str2 = step2(input)14 15 return new WorkflowResponse({16 message: str1,17 })18 }19)20 21export default myWorkflow
Learn more in this documentation.
Learn more in this documentation.
Pass a compensation function that undoes what a step did as a second parameter to createStep
:
1import { 2 createStep,3 StepResponse,4} from "@medusajs/framework/workflows-sdk"5 6const step1 = createStep(7 "step-1",8 async () => {9 const message = `Hello from step one!`10 11 console.log(message)12 13 return new StepResponse(message)14 },15 async () => {16 console.log("Oops! Rolling back my changes...")17 }18)
Learn more in this documentation.
To manipulate variables within a workflow's constructor function, use transform
from the Workflows SDK:
1import { 2 createWorkflow,3 WorkflowResponse,4 transform,5} from "@medusajs/framework/workflows-sdk"6// step imports...7 8const myWorkflow = createWorkflow(9 "hello-world", 10 function (input) {11 const str1 = step1(input)12 const str2 = step2(input)13 14 const str3 = transform(15 { str1, str2 },16 (data) => `${data.str1}${data.str2}`17 )18 19 return new WorkflowResponse(str3)20 }21)
Learn more in this documentation
To perform steps or set a variable's value based on a condition, use when-then
from the Workflows SDK:
1import { 2 createWorkflow,3 WorkflowResponse,4 when,5} from "@medusajs/framework/workflows-sdk"6// step imports...7 8const workflow = createWorkflow(9 "workflow", 10 function (input: {11 is_active: boolean12 }) {13 14 const result = when(15 input, 16 (input) => {17 return input.is_active18 }19 ).then(() => {20 return isActiveStep()21 })22 23 // executed without condition24 const anotherStepResult = anotherStep(result)25 26 return new WorkflowResponse(27 anotherStepResult28 )29 }30)
To run a workflow in another, use the workflow's runAsStep
special method:
1import {2 createWorkflow,3} from "@medusajs/framework/workflows-sdk"4import { 5 createProductsWorkflow,6} from "@medusajs/medusa/core-flows"7 8const workflow = createWorkflow(9 "hello-world",10 async (input) => {11 const products = createProductsWorkflow.runAsStep({12 input: {13 products: [14 // ...15 ],16 },17 })18 19 // ...20 }21)
Learn more in this documentation.
To consume a workflow hook, create a file under src/workflows/hooks
:
1import { createProductsWorkflow } from "@medusajs/medusa/core-flows"2 3createProductsWorkflow.hooks.productsCreated(4 async ({ products, additional_data }, { container }) => {5 // TODO perform an action6 },7 async (dataFromStep, { container }) => {8 // undo the performed action9 }10)
This executes a custom step at the hook's designated point in the workflow.
Learn more in this documentation.
To expose a hook in a workflow, pass it in the second parameter of the returned WorkflowResponse
:
1import {2 createStep,3 createHook,4 createWorkflow,5 WorkflowResponse,6} from "@medusajs/framework/workflows-sdk"7import { createProductStep } from "./steps/create-product"8 9export const myWorkflow = createWorkflow(10 "my-workflow", 11 function (input) {12 const product = createProductStep(input)13 const productCreatedHook = createHook(14 "productCreated", 15 { productId: product.id }16 )17 18 return new WorkflowResponse(product, {19 hooks: [productCreatedHook],20 })21 }22)
Learn more in this documentation.
To configure steps to retry in case of errors, pass the maxRetries
step option:
Learn more in this documentation.
If steps in a workflow don't depend on one another, run them in parallel using parallel
from the Workflows SDK:
1import {2 createWorkflow,3 WorkflowResponse,4 parallelize,5} from "@medusajs/framework/workflows-sdk"6import {7 createProductStep,8 getProductStep,9 createPricesStep,10 attachProductToSalesChannelStep,11} from "./steps"12 13interface WorkflowInput {14 title: string15}16 17const myWorkflow = createWorkflow(18 "my-workflow", 19 (input: WorkflowInput) => {20 const product = createProductStep(input)21 22 const [prices, productSalesChannel] = parallelize(23 createPricesStep(product),24 attachProductToSalesChannelStep(product)25 )26 27 const id = product.id28 const refetchedProduct = getProductStep(product.id)29 30 return new WorkflowResponse(refetchedProduct)31 }32)
Learn more in this documentation.
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:
1import { 2 createStep, 3 createWorkflow,4 WorkflowResponse,5} from "@medusajs/framework/workflows-sdk"6// step import...7 8const myWorkflow = createWorkflow({9 name: "hello-world",10 timeout: 2, // 2 seconds11}, function () {12 const str1 = step1()13 14 return new WorkflowResponse({15 message: str1,16 })17})18 19export default myWorkflow
Learn more in this documentation.
To configure a step's timeout, at which its state changes but its execution isn't stopped, use the timeout
property:
Learn more in this documentation.
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:
Learn more in this documentation.
To change a step's status:
1const workflowEngineService = container.resolve(2 Modules.WORKFLOW_ENGINE3)4 5await workflowEngineService.setStepSuccess({6 idempotencyKey: {7 action: TransactionHandlerType.INVOKE,8 transactionId,9 stepId: "step-2",10 workflowId: "hello-world",11 },12 stepResponse: new StepResponse("Done!"),13 options: {14 container,15 },16})
1const workflowEngineService = container.resolve(2 Modules.WORKFLOW_ENGINE3)4 5await workflowEngineService.setStepFailure({6 idempotencyKey: {7 action: TransactionHandlerType.INVOKE,8 transactionId,9 stepId: "step-2",10 workflowId: "hello-world",11 },12 stepResponse: new StepResponse("Failed!"),13 options: {14 container,15 },16})
Learn more in this documentation.
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:
1import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"2import myWorkflow from "../../../workflows/hello-world"3import { Modules } from "@medusajs/framework/utils"4 5export async function GET(req: MedusaRequest, res: MedusaResponse) {6 const { transaction, result } = await myWorkflow(req.scope).run()7 8 const workflowEngineService = req.scope.resolve(9 Modules.WORKFLOW_ENGINE10 )11 12 const subscriptionOptions = {13 workflowId: "hello-world",14 transactionId: transaction.transactionId,15 subscriberId: "hello-world-subscriber",16 }17 18 await workflowEngineService.subscribe({19 ...subscriptionOptions,20 subscriber: async (data) => {21 if (data.eventType === "onFinish") {22 console.log("Finished execution", data.result)23 // unsubscribe24 await workflowEngineService.unsubscribe({25 ...subscriptionOptions,26 subscriberOrId: subscriptionOptions.subscriberId,27 })28 } else if (data.eventType === "onStepFailure") {29 console.log("Workflow failed", data.step)30 }31 },32 })33 34 res.send(result)35}
Learn more in this documentation.
A subscriber is a function executed whenever the event it listens to is emitted.
To create a subscriber that listens to the product.created
event, create the file src/subscribers/product-created.ts
with the following content:
1import type {2 SubscriberArgs,3 SubscriberConfig,4} from "@medusajs/framework"5 6export default async function productCreateHandler({7 event,8}: SubscriberArgs<{ id: string }>) {9 const productId = event.data.id10 console.log(`The product ${productId} was created`)11}12 13export const config: SubscriberConfig = {14 event: "product.created",15}
Learn more in this documentation.
To resolve resources from the Medusa container in a subscriber, use the container
property of its parameter:
1import { SubscriberArgs, SubscriberConfig } from "@medusajs/framework"2import { Modules } from "@medusajs/framework/utils"3 4export default async function productCreateHandler({5 event: { data },6 container,7}: SubscriberArgs<{ id: string }>) {8 const productModuleService = container.resolve(Modules.PRODUCT)9 10 const productId = data.id11 12 const product = await productModuleService.retrieveProduct(13 productId14 )15 16 console.log(`The product ${product.title} was created`)17}18 19export const config: SubscriberConfig = {20 event: `product.created`,21}
Learn more in this documentation.
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:
1import {2 SubscriberArgs,3 type SubscriberConfig,4} from "@medusajs/medusa"5import { Modules } from "@medusajs/framework/utils"6 7export default async function resetPasswordTokenHandler({8 event: { data: {9 entity_id: email,10 token,11 actor_type,12 } },13 container,14}: SubscriberArgs<{ entity_id: string, token: string, actor_type: string }>) {15 const notificationModuleService = container.resolve(16 Modules.NOTIFICATION17 )18 19 const urlPrefix = actor_type === "customer" ? 20 "https://storefront.com" : 21 "https://admin.com"22 23 await notificationModuleService.createNotifications({24 to: email,25 channel: "email",26 template: "reset-password-template",27 data: {28 // a URL to a frontend application29 url: `${urlPrefix}/reset-password?token=${token}&email=${email}`,30 },31 })32}33 34export const config: SubscriberConfig = {35 event: "auth.password_reset",36}
Learn more in this documentation.
To execute a workflow in a subscriber:
1import {2 type SubscriberConfig,3 type SubscriberArgs,4} from "@medusajs/framework"5import myWorkflow from "../workflows/hello-world"6import { Modules } from "@medusajs/framework/utils"7 8export default async function handleCustomerCreate({9 event: { data },10 container,11}: SubscriberArgs<{ id: string }>) {12 const userId = data.id13 const userModuleService = container.resolve(14 Modules.USER15 )16 17 const user = await userModuleService.retrieveUser(userId)18 19 const { result } = await myWorkflow(container)20 .run({21 input: {22 name: user.first_name,23 },24 })25 26 console.log(result)27}28 29export const config: SubscriberConfig = {30 event: "user.created",31}
Learn more in this documentation
A scheduled job is a function executed at a specified interval of time in the background of your Medusa application.
To create a scheduled job, create the file src/jobs/hello-world.ts
with the following content:
Learn more in this documentation.
To resolve resources in a scheduled job, use the container
accepted as a first parameter:
1import { MedusaContainer } from "@medusajs/framework/types"2import { Modules } from "@medusajs/framework/utils"3 4export default async function myCustomJob(5 container: MedusaContainer6) {7 const productModuleService = container.resolve(Modules.PRODUCT)8 9 const [, count] = await productModuleService.listAndCountProducts()10 11 console.log(12 `Time to check products! You have ${count} product(s)`13 )14}15 16export const config = {17 name: "every-minute-message",18 // execute every minute19 schedule: "* * * * *",20}
Learn more in this documentation
To limit the scheduled job's execution to a number of times during the Medusa application's runtime, use the numberOfExecutions
configuration:
Learn more in this documentation.
To execute a workflow in a scheduled job:
1import { MedusaContainer } from "@medusajs/framework/types"2import myWorkflow from "../workflows/hello-world"3 4export default async function myCustomJob(5 container: MedusaContainer6) {7 const { result } = await myWorkflow(container)8 .run({9 input: {10 name: "John",11 },12 })13 14 console.log(result.message)15}16 17export const config = {18 name: "run-once-a-day",19 schedule: `0 0 * * *`,20}
Learn more in this documentation
A loader is a function defined in a module that's executed when the Medusa application starts.
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:
Learn more in this documentation.
To resolve resources in a loader, use the container
property of its first parameter:
1import {2 LoaderOptions,3} from "@medusajs/framework/types"4import { 5 ContainerRegistrationKeys,6} from "@medusajs/framework/utils"7 8export default async function helloWorldLoader({9 container,10}: LoaderOptions) {11 const logger = container.resolve(ContainerRegistrationKeys.LOGGER)12 13 logger.info("[helloWorldLoader]: Hello, World!")14}
Learn more in this documentation.
To access a module's options in its loader, use the options
property of its first parameter:
1import {2 LoaderOptions,3} from "@medusajs/framework/types"4 5// recommended to define type in another file6type ModuleOptions = {7 apiKey?: boolean8}9 10export default async function helloWorldLoader({11 options,12}: LoaderOptions<ModuleOptions>) {13 14 console.log(15 "[HELLO MODULE] Just started the Medusa application!",16 options17 )18}
Learn more in this documentation.
To register a resource in the Module's container using a loader, use the container
's registerAdd
method:
Where the first parameter of registerAdd
is the name to register the resource under, and the second parameter is the resource to register.
You can customize the Medusa Admin to inject widgets in existing pages, or create new pages using UI routes.
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:
1import { defineWidgetConfig } from "@medusajs/admin-sdk"2import { Container, Heading } from "@medusajs/ui"3 4const ProductWidget = () => {5 return (6 <Container className="divide-y p-0">7 <div className="flex items-center justify-between px-6 py-4">8 <Heading level="h2">Product Widget</Heading>9 </div>10 </Container>11 )12}13 14export const config = defineWidgetConfig({15 zone: "product.list.before",16})17 18export default ProductWidget
Learn more about widgets in this documentation.
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):
1import { defineWidgetConfig } from "@medusajs/admin-sdk"2import { Container, Heading } from "@medusajs/ui"3import { 4 DetailWidgetProps, 5 AdminProduct,6} from "@medusajs/framework/types"7 8// The widget9const ProductWidget = ({ 10 data,11}: DetailWidgetProps<AdminProduct>) => {12 return (13 <Container className="divide-y p-0">14 <div className="flex items-center justify-between px-6 py-4">15 <Heading level="h2">16 Product Widget {data.title}17 </Heading>18 </div>19 </Container>20 )21}22 23// The widget's configurations24export const config = defineWidgetConfig({25 zone: "product.details.before",26})27 28export default ProductWidget
Learn more in this documentation.
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:
1import { defineRouteConfig } from "@medusajs/admin-sdk"2import { ChatBubbleLeftRight } from "@medusajs/icons"3import { Container, Heading } from "@medusajs/ui"4 5const CustomPage = () => {6 return (7 <Container className="divide-y p-0">8 <div className="flex items-center justify-between px-6 py-4">9 <Heading level="h2">This is my custom route</Heading>10 </div>11 </Container>12 )13}14 15export const config = defineRouteConfig({16 label: "Custom Route",17 icon: ChatBubbleLeftRight,18})19 20export default CustomPage
This adds a new page at localhost:9000/app/custom
.
Learn more in this documentation.
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:
1import { defineRouteConfig } from "@medusajs/admin-sdk"2import { Container, Heading } from "@medusajs/ui"3 4const CustomSettingPage = () => {5 return (6 <Container className="divide-y p-0">7 <div className="flex items-center justify-between px-6 py-4">8 <Heading level="h1">Custom Setting Page</Heading>9 </div>10 </Container>11 )12}13 14export const config = defineRouteConfig({15 label: "Custom",16})17 18export default CustomSettingPage
This adds a setting page at localhost:9000/app/settings/custom
.
Learn more in this documentation
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:
1import { useParams } from "react-router-dom"2import { Container } from "@medusajs/ui"3 4const CustomPage = () => {5 const { id } = useParams()6 7 return (8 <Container className="divide-y p-0">9 <div className="flex items-center justify-between px-6 py-4">10 <Heading level="h1">Passed ID: {id}</Heading>11 </div>12 </Container>13 )14}15 16export default CustomPage
This creates a UI route at localhost:9000/app/custom/:id
, where :id
is a path parameter.
Learn more in this documentation
To send a request to custom API routes from the admin dashboard, use the Fetch API.
For example:
1import { defineWidgetConfig } from "@medusajs/admin-sdk"2import { Container } from "@medusajs/ui"3import { useEffect, useState } from "react"4 5const ProductWidget = () => {6 const [productsCount, setProductsCount] = useState(0)7 const [loading, setLoading] = useState(true)8 9 useEffect(() => {10 if (!loading) {11 return12 }13 14 fetch(`/admin/products`, {15 credentials: "include",16 })17 .then((res) => res.json())18 .then(({ count }) => {19 setProductsCount(count)20 setLoading(false)21 })22 }, [loading])23 24 return (25 <Container className="divide-y p-0">26 {loading && <span>Loading...</span>}27 {!loading && <span>You have {productsCount} Product(s).</span>}28 </Container>29 )30}31 32export const config = defineWidgetConfig({33 zone: "product.list.before",34})35 36export default ProductWidget
Learn more in this documentation
To add a link to another page in a UI route or a widget, use react-router-dom
's Link
component:
1import { defineWidgetConfig } from "@medusajs/admin-sdk"2import { Container } from "@medusajs/ui"3import { Link } from "react-router-dom"4 5// The widget6const ProductWidget = () => {7 return (8 <Container className="divide-y p-0">9 <Link to={"/orders"}>View Orders</Link>10 </Container>11 )12}13 14// The widget's configurations15export const config = defineWidgetConfig({16 zone: "product.details.before",17})18 19export default ProductWidget
Learn more in this documentation.
Medusa provides a @medusajs/test-utils
package with utility tools to create integration tests for your custom API routes, modules, or other Medusa customizations.
To create a test for a custom API route, create the file integration-tests/http/custom-routes.spec.ts
with the following content:
1import { medusaIntegrationTestRunner } from "@medusajs/test-utils"2 3medusaIntegrationTestRunner({4 testSuite: ({ api, getContainer }) => {5 describe("Custom endpoints", () => {6 describe("GET /custom", () => {7 it("returns correct message", async () => {8 const response = await api.get(9 `/custom`10 )11 12 expect(response.status).toEqual(200)13 expect(response.data).toHaveProperty("message")14 expect(response.data.message).toEqual("Hello, World!")15 })16 })17 })18 },19})
Then, run the test with the following command:
Learn more in this documentation.
To create a test for a workflow, create the file integration-tests/http/workflow.spec.ts
with the following content:
1import { medusaIntegrationTestRunner } from "@medusajs/test-utils"2import { helloWorldWorkflow } from "../../src/workflows/hello-world"3 4medusaIntegrationTestRunner({5 testSuite: ({ getContainer }) => {6 describe("Test hello-world workflow", () => {7 it("returns message", async () => {8 const { result } = await helloWorldWorkflow(getContainer())9 .run()10 11 expect(result).toEqual("Hello, World!")12 })13 })14 },15})
Then, run the test with the following command:
Learn more in this documentation.
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/hello/__tests__/service.spec.ts
with the following content:
1import { moduleIntegrationTestRunner } from "@medusajs/test-utils"2import { HELLO_MODULE } from ".."3import HelloModuleService from "../service"4import MyCustom from "../models/my-custom"5 6moduleIntegrationTestRunner<HelloModuleService>({7 moduleName: HELLO_MODULE,8 moduleModels: [MyCustom],9 resolve: "./modules/hello",10 testSuite: ({ service }) => {11 describe("HelloModuleService", () => {12 it("says hello world", () => {13 const message = service.getMessage()14 15 expect(message).toEqual("Hello, World!")16 })17 })18 },19})
Then, run the test with the following command:
Medusa provides all its commerce features as separate commerce modules, such as the Product or Order modules.
To create an actor type that can authenticate to the Medusa application, such as a manager
:
setAuthAppMetadataStep
as a step in a workflow that creates a manager:1import { 2 createWorkflow, 3 WorkflowResponse,4} from "@medusajs/framework/workflows-sdk"5import { 6 setAuthAppMetadataStep,7} from "@medusajs/medusa/core-flows"8// other imports...9 10const createManagerWorkflow = createWorkflow(11 "create-manager",12 function (input: CreateManagerWorkflowInput) {13 const manager = createManagerStep({14 manager: input.manager,15 })16 17 setAuthAppMetadataStep({18 authIdentityId: input.authIdentityId,19 actorType: "manager",20 value: manager.id,21 })22 23 return new WorkflowResponse(manager)24 }25)
1import type { 2 AuthenticatedMedusaRequest,3 MedusaResponse,4} from "@medusajs/framework/http"5import { MedusaError } from "@medusajs/framework/utils"6import createManagerWorkflow from "../../workflows/create-manager"7 8type RequestBody = {9 first_name: string10 last_name: string11 email: string12}13 14export async function POST(15 req: AuthenticatedMedusaRequest<RequestBody>, 16 res: MedusaResponse17) {18 // If `actor_id` is present, the request carries 19 // authentication for an existing manager20 if (req.auth_context.actor_id) {21 throw new MedusaError(22 MedusaError.Types.INVALID_DATA,23 "Request already authenticated as a manager."24 )25 }26 27 const { result } = await createManagerWorkflow(req.scope)28 .run({29 input: {30 manager: req.body,31 authIdentityId: req.auth_context.auth_identity_id,32 },33 })34 35 res.status(200).json({ manager: result })36}
authenticate
middleware on the new route in src/api/middlewares.ts
:1import { 2 defineMiddlewares,3 authenticate,4} from "@medusajs/framework/http"5 6export default defineMiddlewares({7 routes: [8 {9 matcher: "/manager",10 method: "POST",11 middlewares: [12 authenticate("manager", ["session", "bearer"], {13 allowUnregistered: true,14 }),15 ],16 },17 {18 matcher: "/manager/me*",19 middlewares: [20 authenticate("manager", ["session", "bearer"]),21 ],22 },23 ],24})
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.
To apply a promotion on a cart's items and shipping methods using the Cart and Promotion modules:
1import {2 ComputeActionAdjustmentLine,3 ComputeActionItemLine,4 ComputeActionShippingLine,5 AddItemAdjustmentAction,6 AddShippingMethodAdjustment,7 // ...8} from "@medusajs/framework/types"9 10// retrieve the cart11const cart = await cartModuleService.retrieveCart("cart_123", {12 relations: [13 "items.adjustments",14 "shipping_methods.adjustments",15 ],16})17 18// retrieve line item adjustments19const lineItemAdjustments: ComputeActionItemLine[] = []20cart.items.forEach((item) => {21 const filteredAdjustments = item.adjustments?.filter(22 (adjustment) => adjustment.code !== undefined23 ) as unknown as ComputeActionAdjustmentLine[]24 if (filteredAdjustments.length) {25 lineItemAdjustments.push({26 ...item,27 adjustments: filteredAdjustments,28 })29 }30})31 32// retrieve shipping method adjustments33const shippingMethodAdjustments: ComputeActionShippingLine[] =34 []35cart.shipping_methods.forEach((shippingMethod) => {36 const filteredAdjustments =37 shippingMethod.adjustments?.filter(38 (adjustment) => adjustment.code !== undefined39 ) as unknown as ComputeActionAdjustmentLine[]40 if (filteredAdjustments.length) {41 shippingMethodAdjustments.push({42 ...shippingMethod,43 adjustments: filteredAdjustments,44 })45 }46})47 48// compute actions49const actions = await promotionModuleService.computeActions(50 ["promo_123"],51 {52 items: lineItemAdjustments,53 shipping_methods: shippingMethodAdjustments,54 }55)56 57// set the adjustments on the line item58await cartModuleService.setLineItemAdjustments(59 cart.id,60 actions.filter(61 (action) => action.action === "addItemAdjustment"62 ) as AddItemAdjustmentAction[]63)64 65// set the adjustments on the shipping method66await cartModuleService.setShippingMethodAdjustments(67 cart.id,68 actions.filter(69 (action) =>70 action.action === "addShippingMethodAdjustment"71 ) as AddShippingMethodAdjustment[]72)
Learn more in this documentation.
To retrieve the tax lines of a cart's items and shipping methods using the Cart and Tax modules:
1// retrieve the cart2const cart = await cartModuleService.retrieveCart("cart_123", {3 relations: [4 "items.tax_lines",5 "shipping_methods.tax_lines",6 "shipping_address",7 ],8})9 10// retrieve the tax lines11const taxLines = await taxModuleService.getTaxLines(12 [13 ...(cart.items as TaxableItemDTO[]),14 ...(cart.shipping_methods as TaxableShippingDTO[]),15 ],16 {17 address: {18 ...cart.shipping_address,19 country_code:20 cart.shipping_address.country_code || "us",21 },22 }23)24 25// set line item tax lines26await cartModuleService.setLineItemTaxLines(27 cart.id,28 taxLines.filter((line) => "line_item_id" in line)29)30 31// set shipping method tax lines32await cartModuleService.setLineItemTaxLines(33 cart.id,34 taxLines.filter((line) => "shipping_line_id" in line)35)
Learn more in this documentation
To apply a promotion on an order's items and shipping methods using the Order and Promotion modules:
1import {2 ComputeActionAdjustmentLine,3 ComputeActionItemLine,4 ComputeActionShippingLine,5 AddItemAdjustmentAction,6 AddShippingMethodAdjustment,7 // ...8} from "@medusajs/framework/types"9 10// ...11 12// retrieve the order13const order = await orderModuleService.retrieveOrder("ord_123", {14 relations: [15 "items.item.adjustments",16 "shipping_methods.shipping_method.adjustments",17 ],18})19// retrieve the line item adjustments20const lineItemAdjustments: ComputeActionItemLine[] = []21order.items.forEach((item) => {22 const filteredAdjustments = item.adjustments?.filter(23 (adjustment) => adjustment.code !== undefined24 ) as unknown as ComputeActionAdjustmentLine[]25 if (filteredAdjustments.length) {26 lineItemAdjustments.push({27 ...item,28 ...item.detail,29 adjustments: filteredAdjustments,30 })31 }32})33 34//retrieve shipping method adjustments35const shippingMethodAdjustments: ComputeActionShippingLine[] =36 []37order.shipping_methods.forEach((shippingMethod) => {38 const filteredAdjustments =39 shippingMethod.adjustments?.filter(40 (adjustment) => adjustment.code !== undefined41 ) as unknown as ComputeActionAdjustmentLine[]42 if (filteredAdjustments.length) {43 shippingMethodAdjustments.push({44 ...shippingMethod,45 adjustments: filteredAdjustments,46 })47 }48})49 50// compute actions51const actions = await promotionModuleService.computeActions(52 ["promo_123"],53 {54 items: lineItemAdjustments,55 shipping_methods: shippingMethodAdjustments,56 // TODO infer from cart or region57 currency_code: "usd",58 }59)60 61// set the adjustments on the line items62await orderModuleService.setOrderLineItemAdjustments(63 order.id,64 actions.filter(65 (action) => action.action === "addItemAdjustment"66 ) as AddItemAdjustmentAction[]67)68 69// set the adjustments on the shipping methods70await orderModuleService.setOrderShippingMethodAdjustments(71 order.id,72 actions.filter(73 (action) =>74 action.action === "addShippingMethodAdjustment"75 ) as AddShippingMethodAdjustment[]76)
Learn more in this documentation
To accept payment using the Payment Module's main service:
1import { 2 ContainerRegistrationKeys,3 Modules,4} from "@medusajs/framework/utils"5 6// ...7 8const paymentCollection =9 await paymentModuleService.createPaymentCollections({10 region_id: "reg_123",11 currency_code: "usd",12 amount: 5000,13 })14 15// resolve Link16const link = container.resolve(17 ContainerRegistrationKeys.LINK18)19 20// create a link between the cart and payment collection21link.create({22 [Modules.CART]: {23 cart_id: "cart_123",24 },25 [Modules.PAYMENT]: {26 payment_collection_id: paymentCollection.id,27 },28})
Learn more in this documentation.
To get prices of a product variant for a region and currency using Query:
1import { QueryContext } from "@medusajs/framework/utils"2 3// ...4 5const { data: products } = await query.graph({6 entity: "product",7 fields: [8 "*",9 "variants.*",10 "variants.calculated_price.*",11 ],12 filters: {13 id: "prod_123",14 },15 context: {16 variants: {17 calculated_price: QueryContext({18 region_id: "reg_01J3MRPDNXXXDSCC76Y6YCZARS",19 currency_code: "eur",20 }),21 },22 },23})
Learn more in this documentation.
To get all prices of a product variant using Query:
Learn more in this documentation.
To get a variant's prices with taxes using Query and the Tax Module
1import {2 HttpTypes,3 TaxableItemDTO,4 ItemTaxLineDTO,5} from "@medusajs/framework/types"6import { 7 QueryContext,8 calculateAmountsWithTax,9} from "@medusajs/framework/utils"10// other imports...11 12// ...13const asTaxItem = (product: HttpTypes.StoreProduct): TaxableItemDTO[] => {14 return product.variants15 ?.map((variant) => {16 if (!variant.calculated_price) {17 return18 }19 20 return {21 id: variant.id,22 product_id: product.id,23 product_name: product.title,24 product_categories: product.categories?.map((c) => c.name),25 product_category_id: product.categories?.[0]?.id,26 product_sku: variant.sku,27 product_type: product.type,28 product_type_id: product.type_id,29 quantity: 1,30 unit_price: variant.calculated_price.calculated_amount,31 currency_code: variant.calculated_price.currency_code,32 }33 })34 .filter((v) => !!v) as unknown as TaxableItemDTO[]35}36 37const { data: products } = await query.graph({38 entity: "product",39 fields: [40 "*",41 "variants.*",42 "variants.calculated_price.*",43 ],44 filters: {45 id: "prod_123",46 },47 context: {48 variants: {49 calculated_price: QueryContext({50 region_id: "region_123",51 currency_code: "usd",52 }),53 },54 },55})56 57const taxLines = (await taxModuleService.getTaxLines(58 products.map(asTaxItem).flat(),59 {60 // example of context properties. You can pass other ones.61 address: {62 country_code,63 },64 }65)) as unknown as ItemTaxLineDTO[]66 67const taxLinesMap = new Map<string, ItemTaxLineDTO[]>()68taxLines.forEach((taxLine) => {69 const variantId = taxLine.line_item_id70 if (!taxLinesMap.has(variantId)) {71 taxLinesMap.set(variantId, [])72 }73 74 taxLinesMap.get(variantId)?.push(taxLine)75})76 77products.forEach((product) => {78 product.variants?.forEach((variant) => {79 if (!variant.calculated_price) {80 return81 }82 83 const taxLinesForVariant = taxLinesMap.get(variant.id) || []84 const { priceWithTax, priceWithoutTax } = calculateAmountsWithTax({85 taxLines: taxLinesForVariant,86 amount: variant.calculated_price!.calculated_amount!,87 includesTax:88 variant.calculated_price!.is_calculated_price_tax_inclusive!,89 })90 91 // do something with prices...92 })93})
Learn more in this documentation.
To invite a user using the User Module:
To accept an invite and create a user using the User Module: