docs: docs for next release (#13621)

* docs: docs for next release

* changes to opentelemetry dependencies

* document plugin env variables

* document admin changes

* fix vale error

* add version notes

* document campaign budget updates

* document campaign changes in user guide

* document chages in cluster mode cli

* documented once promotion allocation

* document multiple API keys support
This commit is contained in:
Shahed Nasser
2025-10-21 10:32:08 +03:00
committed by GitHub
parent f38f0f9aca
commit ed715813a5
54 changed files with 1621 additions and 252 deletions

View File

@@ -0,0 +1,21 @@
import { ChildDocs } from "docs-ui"
export const metadata = {
title: `${pageNumber} Medusa Codemods`,
}
# {metadata.title}
In this chapter, you'll learn about Medusa codemods and the list of available codemods.
## What are Codemods?
Codemods are scripts that help you automate codebase changes. They are especially useful when updating to a new version that requires large changes to your codebase.
Medusa provides codemods to help you update your codebase. Use these codemods when updating to their respective versions.
---
## List of Medusa Codemods
<ChildDocs type="item" />

View File

@@ -0,0 +1,279 @@
export const metadata = {
title: `${pageNumber} Replace Imports Codemod (v2.11.0+)`,
}
# {metadata.title}
In this chapter, you'll learn about the codemod that helps you replace imports in your codebase when upgrading to Medusa v2.11.0.
## What is the Replace Imports Codemod?
[Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0) optimized the package structure by consolidating several external packages into the `@medusajs/framework` package.
Previously, you had to install and manage packages related to MikroORM, Awilix, OpenTelemetry, and the `pg` package separately in your Medusa application. Starting with v2.11.0, these packages are included in the `@medusajs/framework` package.
For example, instead of importing `@mikro-orm/core`, you now import it from `@medusajs/framework/mikro-orm/core`. This applies to all of the following packages:
- `@mikro-orm/*` packages (for example, `@mikro-orm/core`, `@mikro-orm/migrations`, etc.) -> `@medusajs/framework/mikro-orm/{subpath}`
- `awilix` -> `@medusajs/framework/awilix`
- `pg` -> `@medusajs/framework/pg`
- `@opentelemetry/instrumentation-pg` -> `@medusajs/framework/opentelemetry/instrumentation-pg`
- `@opentelemetry/resources` -> `@medusajs/framework/opentelemetry/resources`
- `@opentelemetry/sdk-node` -> `@medusajs/framework/opentelemetry/sdk-node`
- `@opentelemetry/sdk-trace-node` -> `@medusajs/framework/opentelemetry/sdk-trace-node`
To help you update your codebase to reflect these changes, Medusa provides a codemod that automatically replaces imports of these packages throughout your codebase.
---
## Using the Replace Imports Codemod
To use the replace imports codemod, create the file `replace-imports.js` in the root of your Medusa application with the following content:
```js
#!/usr/bin/env node
const fs = require("fs")
const path = require("path")
const { execSync } = require("child_process")
/**
* Script to replace imports and require statements from mikro-orm/{subpath}, awilix, and pg
* to their @medusajs/framework equivalents
*/
// Define the replacement mappings
const replacements = [
// MikroORM imports - replace mikro-orm/{subpath} with @medusajs/framework/mikro-orm/{subpath}
{
pattern: /from\s+['"]@?mikro-orm\/([^'"]+)['"]/g,
// eslint-disable-next-line quotes
replacement: 'from "@medusajs/framework/mikro-orm/$1"',
},
// Awilix imports - replace awilix with @medusajs/framework/awilix
{
pattern: /from\s+['"]awilix['"]/g,
// eslint-disable-next-line quotes
replacement: 'from "@medusajs/framework/awilix"',
},
// PG imports - replace pg with @medusajs/framework/pg
{
pattern: /from\s+['"]pg['"]/g,
// eslint-disable-next-line quotes
replacement: 'from "@medusajs/framework/pg"',
},
// OpenTelemetry imports - replace @opentelemetry/instrumentation-pg, @opentelemetry/resources,
// @opentelemetry/sdk-node, and @opentelemetry/sdk-trace-node with @medusajs/framework/opentelemetry/{subpath}
{
pattern: /from\s+['"]@?opentelemetry\/(instrumentation-pg|resources|sdk-node|sdk-trace-node)['"]/g,
// eslint-disable-next-line quotes
replacement: 'from "@medusajs/framework/opentelemetry/$1"',
},
// MikroORM require statements - replace require('@?mikro-orm/{subpath}') with require('@medusajs/framework/mikro-orm/{subpath}')
{
pattern: /require\s*\(\s*['"]@?mikro-orm\/([^'"]+)['"]\s*\)/g,
// eslint-disable-next-line quotes
replacement: 'require("@medusajs/framework/mikro-orm/$1")',
},
// Awilix require statements - replace require('awilix') with require('@medusajs/framework/awilix')
{
pattern: /require\s*\(\s*['"]awilix['"]\s*\)/g,
// eslint-disable-next-line quotes
replacement: 'require("@medusajs/framework/awilix")',
},
// PG require statements - replace require('pg') with require('@medusajs/framework/pg')
{
pattern: /require\s*\(\s*['"]pg['"]\s*\)/g,
// eslint-disable-next-line quotes
replacement: 'require("@medusajs/framework/pg")',
},
// OpenTelemetry require statements - replace require('@opentelemetry/instrumentation-pg'),
// require('@opentelemetry/resources'), require('@opentelemetry/sdk-node'), and
// require('@opentelemetry/sdk-trace-node') with require('@medusajs/framework/opentelemetry/{subpath}')
{
pattern: /require\s*\(\s*['"]@?opentelemetry\/(instrumentation-pg|resources|sdk-node|sdk-trace-node)['"]\s*\)/g,
// eslint-disable-next-line quotes
replacement: 'require("@medusajs/framework/opentelemetry/$1")',
},
]
function processFile(filePath) {
try {
const content = fs.readFileSync(filePath, "utf8")
let modifiedContent = content
let wasModified = false
replacements.forEach(({ pattern, replacement }) => {
const newContent = modifiedContent.replace(pattern, replacement)
if (newContent !== modifiedContent) {
wasModified = true
modifiedContent = newContent
}
})
if (wasModified) {
fs.writeFileSync(filePath, modifiedContent)
console.log(`✓ Updated: ${filePath}`)
return true
}
return false
} catch (error) {
console.error(`✗ Error processing ${filePath}:`, error.message)
return false
}
}
function getTargetFiles() {
try {
// Get the current script's filename to exclude it from processing
const currentScript = path.basename(__filename)
// Find TypeScript/JavaScript files, excluding common directories that typically don't contain target imports
const findCommand = `find . -name node_modules -prune -o -name .git -prune -o -name dist -prune -o -name build -prune -o -name coverage -prune -o -name "*.ts" -print -o -name "*.js" -print -o -name "*.tsx" -print -o -name "*.jsx" -print`
const files = execSync(findCommand, {
encoding: "utf8",
maxBuffer: 50 * 1024 * 1024, // 50MB buffer
})
.split("\n")
.filter((line) => line.trim())
console.log(files)
const targetFiles = []
let processedCount = 0
console.log(`📄 Scanning ${files.length} files for target imports and require statements...`)
for (const file of files) {
try {
// Skip the current script file
const fileName = path.basename(file)
if (fileName === currentScript) {
processedCount++
continue
}
const content = fs.readFileSync(file, "utf8")
if (
/from\s+['"]@?mikro-orm\//.test(content) ||
/from\s+['"]awilix['"]/.test(content) ||
/from\s+['"]pg['"]/.test(content) ||
/require\s*\(\s*['"]@?mikro-orm\//.test(content) ||
/require\s*\(\s*['"]awilix['"]/.test(content) ||
/require\s*\(\s*['"]pg['"]/.test(content)
) {
targetFiles.push(file.startsWith("./") ? file.slice(2) : file)
}
processedCount++
if (processedCount % 100 === 0) {
process.stdout.write(
`\r📄 Processed ${processedCount}/${files.length} files...`
)
}
} catch (fileError) {
// Skip files that can't be read
continue
}
}
if (processedCount > 0) {
console.log(`\r📄 Processed ${processedCount} files. `)
}
return targetFiles
} catch (error) {
console.error("Error finding target files:", error.message)
return []
}
}
function main() {
console.log("🔄 Finding files with target imports and require statements...")
const targetFiles = getTargetFiles()
if (targetFiles.length === 0) {
console.log(" No files found with target imports or require statements.")
return
}
console.log(`📁 Found ${targetFiles.length} files to process`)
let modifiedCount = 0
let errorCount = 0
targetFiles.forEach((filePath) => {
const fullPath = path.resolve(filePath)
if (fs.existsSync(fullPath)) {
if (processFile(fullPath)) {
modifiedCount++
}
} else {
console.warn(`⚠️ File not found: ${filePath}`)
errorCount++
}
})
console.log("\n📊 Summary:")
console.log(` Files processed: ${targetFiles.length}`)
console.log(` Files modified: ${modifiedCount}`)
console.log(` Errors: ${errorCount}`)
if (modifiedCount > 0) {
console.log("\n✅ Import replacement completed successfully!")
console.log("\n💡 Next steps:")
console.log(" 1. Review the changes with: git diff")
console.log(" 2. Run your tests to ensure everything works correctly")
console.log(" 3. Commit the changes if you're satisfied")
} else {
console.log(
"\n✅ No modifications needed - all imports are already correct!"
)
}
}
// Run if called directly
if (require.main === module) {
main()
}
module.exports = { processFile, getTargetFiles, main }
```
This script scans your project for files that import from `mikro-orm/{subpath}`, `awilix`, or `pg`, and replaces those imports with their new equivalents from `@medusajs/framework`. It handles both ES module `import` statements and CommonJS `require` statements in JavaScript and TypeScript files.
Next, run the following command in your terminal to make the script executable:
<Note title="Windows Users">
You can run the script using `node` without changing permissions.
</Note>
```bash
chmod +x replace-imports.js
```
Finally, execute the script with the following command:
```bash
node replace-imports.js
```
This will scan your project files, apply the necessary import replacements, and provide a summary of the changes made.
---
## Next Steps
After running the codemod, review the changes made to your codebase. You can use `git diff` to see the modifications. Additionally, run your tests to ensure everything works as expected.
If everything is working correctly, you can remove the `replace-imports.js` file from your project. You can also remove the following packages from your `package.json`, as they're now included in the `@medusajs/framework` package:
- `@mikro-orm/*` packages (for example, `@mikro-orm/core`, `@mikro-orm/migrations`, etc.)
- `awilix`
- `pg`
- `@opentelemetry/instrumentation-pg`
- `@opentelemetry/resources`
- `@opentelemetry/sdk-node`
- `@opentelemetry/sdk-trace-node`

View File

@@ -40,13 +40,13 @@ Medusa uses [OpenTelemetry](https://opentelemetry.io/) for instrumentation and r
### Install Dependencies
Start by installing the following OpenTelemetry dependencies in your Medusa project:
<Note>
```bash npm2yarn
npm install @opentelemetry/sdk-node @opentelemetry/resources @opentelemetry/sdk-trace-node @opentelemetry/instrumentation-pg
```
As of [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0), OpenTelemetry dependencies are installed by default in new Medusa projects. If you're using an older version of Medusa, you need to install the `@opentelemetry/sdk-node`, `@opentelemetry/resources`, `@opentelemetry/sdk-trace-node`, and `@opentelemetry/instrumentation-pg` dependencies.
Also, install the dependencies relevant for the exporter you use. If you're using Zipkin, install the following dependencies:
</Note>
Before you start, you must install the dependencies relevant for the exporter you use. If you're using Zipkin, install the following dependencies:
```bash npm2yarn
npm install @opentelemetry/exporter-zipkin

View File

@@ -63,8 +63,14 @@ if (process.env.TEST_TYPE === "integration:http") {
Next, create the `integration-tests/setup.js` file with the following content:
<Note>
As of [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0), MikroORM dependencies are included in the `@medusajs/framework` package. If you're using an older version of Medusa, change the require statement to `@mikro-orm/core`.
</Note>
```js title="integration-tests/setup.js"
const { MetadataStorage } = require("@mikro-orm/core")
const { MetadataStorage } = require("@medusajs/framework/@mikro-orm/core")
MetadataStorage.clear()
```

View File

@@ -98,9 +98,48 @@ For example, the `VITE_MY_API_KEY` environment variable in the example above wil
## Environment Variables in Plugins
As explained in the [previous section](#environment-variables-in-production), environment variables are inlined into the build. This presents a limitation for plugins, where you can't use environment variables.
<Note>
Instead, only the following global variable is available in plugins:
Environment variable support in plugins is available starting [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0). Refer to the [Medusa versions prior to v2.11.0](#for-medusa-versions-prior-to-v2110) section for more details if you're using an earlier version.
</Note>
For plugins, you can use environment variables without a prefix. Then, Medusa applications that use the plugin can set the environment variable with the `PLUGIN_` prefix.
For example, you can create a widget in your plugin that uses the `MY_API_KEY` environment variable:
```tsx highlights={[["8"]]}
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container, Heading } from "@medusajs/ui"
const ProductWidget = () => {
return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">API Key: {import.meta.env.MY_API_KEY}</Heading>
</div>
</Container>
)
}
export const config = defineWidgetConfig({
zone: "product.details.before",
})
export default ProductWidget
```
Then, in the Medusa application that uses the plugin, set the environment variable with the `PLUGIN_` prefix:
```bash
PLUGIN_MY_API_KEY=sk_123
```
The `MY_API_KEY` environment variable in the plugin will be replaced with the value of `PLUGIN_MY_API_KEY` during the build process of the Medusa application.
### Global Variables in Plugins
Plugins also have the following global variables available:
- `__BACKEND_URL__`: The URL of the Medusa backend, as set in the [Medusa configurations](../../../configurations/medusa-config/page.mdx#backendurl).
- `__BASE__`: The base path of the Medusa Admin. (For example, `/app`).
@@ -137,4 +176,14 @@ To fix possible type errors, create the file `src/admin/vite-env.d.ts` and add t
declare const __BACKEND_URL__: string
declare const __BASE__: string
declare const __STOREFRONT_URL__: string
```
```
### For Medusa versions prior to v2.11.0
<Details summaryContent="Instructions for Medusa versions prior to v2.11.0">
As explained in the [Environment Variables in Production section](#environment-variables-in-production), environment variables are inlined into the build. This presents a limitation for plugins, where you can't use environment variables.
Instead, you can use the [Plugin Global Variables](#global-variables-in-plugins) described above to access the backend URL, base path, and storefront URL.
</Details>

View File

@@ -63,8 +63,14 @@ npx medusa db:generate blog
The `db:generate` command of the Medusa CLI accepts one or more module names to generate the migration for. It will create a migration file for the Blog Module in the directory `src/modules/blog/migrations` similar to the following:
<Note>
As of [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0), MikroORM dependencies are included in the `@medusajs/framework` package. If you're using an older version of Medusa, change the import statement to `@mikro-orm/migrations`.
</Note>
```ts
import { Migration } from "@mikro-orm/migrations"
import { Migration } from "@medusajs/framework/@mikro-orm/migrations"
export class Migration20241121103722 extends Migration {

View File

@@ -37,8 +37,14 @@ You can also write migrations manually. To do that, create a file in the `migrat
For example:
<Note>
As of [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0), MikroORM dependencies are included in the `@medusajs/framework` package. If you're using an older version of Medusa, change the import statement to `@mikro-orm/migrations`.
</Note>
```ts title="src/modules/blog/migrations/Migration202507021059_create_author.ts"
import { Migration } from "@mikro-orm/migrations"
import { Migration } from "@medusajs/framework/@mikro-orm/migrations"
export class Migration202507021059 extends Migration {
@@ -53,7 +59,7 @@ export class Migration202507021059 extends Migration {
}
```
The migration class in the file extends the `Migration` class imported from `@mikro-orm/migrations`. In the `up` and `down` method of the migration class, you use the `addSql` method provided by MikroORM's `Migration` class to run PostgreSQL syntax.
The migration class in the file extends the `Migration` class imported from `@medusajs/framework/@mikro-orm/migrations`. In the `up` and `down` method of the migration class, you use the `addSql` method provided by MikroORM's `Migration` class to run PostgreSQL syntax.
In the example above, the `up` method creates the table `author`, and the `down` method drops the table if the migration is reverted.

View File

@@ -27,6 +27,12 @@ So, to run database queries in a service:
For example, in your service, add the following methods:
<Note>
As of [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0), MikroORM dependencies are included in the `@medusajs/framework` package. If you're using an older version of Medusa, change the import statement to `@mikro-orm/knex`.
</Note>
export const methodsHighlight = [
["13", "getCount", "Retrieves the number of records in `my_custom` using the `count` method."],
["20", "getCountSql", "Retrieves the number of records in `my_custom` using the `execute` method."]
@@ -39,7 +45,7 @@ import {
MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
class BlogModuleService {
// ...
@@ -66,7 +72,7 @@ class BlogModuleService {
You add two methods `getCount` and `getCountSql` that have the `InjectManager` decorator. Each of the methods also accept the `sharedContext` parameter which has the `MedusaContext` decorator.
The entity manager is injected to the `sharedContext.manager` property, which is an instance of [EntityManager from the @mikro-orm/knex package](https://mikro-orm.io/api/knex/class/EntityManager).
The entity manager is injected to the `sharedContext.manager` property, which is an instance of [EntityManager from the `@medusajs/framework/@mikro-orm/knex` package](https://mikro-orm.io/api/knex/class/EntityManager).
You use the manager in the `getCount` method to retrieve the number of records in a table, and in the `getCountSql` to run a PostgreSQL query that retrieves the count.
@@ -113,7 +119,7 @@ import {
MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
class BlogModuleService {
// ...
@@ -174,7 +180,7 @@ import {
MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
class BlogModuleService {
// ...
@@ -221,7 +227,7 @@ import {
MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
class BlogModuleService {
// ...
@@ -380,7 +386,7 @@ import {
MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
class BlogModuleService {
// ...
@@ -512,7 +518,7 @@ import {
MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
class BlogModuleService {
// ...
@@ -582,7 +588,7 @@ The second parameter of the `baseRepository_.transaction` method is an object of
```ts highlights={[["24"]]}
// other imports...
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
import {
InjectTransactionManager,
MedusaContext,
@@ -625,8 +631,8 @@ import {
MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { IsolationLevel } from "@mikro-orm/core"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
import { IsolationLevel } from "@medusajs/framework/@mikro-orm/core"
class BlogModuleService {
// ...
@@ -660,7 +666,7 @@ import {
MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
class BlogModuleService {
// ...

View File

@@ -152,6 +152,12 @@ Consider your have a MongoDB module that allows you to perform operations on a M
To connect to the database, you create the following loader in your module:
<Note>
As of [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0), Awilix dependencies are included in the `@medusajs/framework` package. If you're using an older version of Medusa, change the import statement to `awilix`.
</Note>
export const loaderHighlights = [
["5", "ModuleOptions", "Define a type for expected options."],
["13", "ModuleOptions", "Pass the option type as a type argument to `LoaderOptions`."],
@@ -163,7 +169,7 @@ export const loaderHighlights = [
```ts title="src/modules/mongo/loaders/connection.ts" highlights={loaderHighlights}
import { LoaderOptions } from "@medusajs/framework/types"
import { asValue } from "awilix"
import { asValue } from "@medusajs/framework/awilix"
import { MongoClient } from "mongodb"
type ModuleOptions = {
@@ -233,7 +239,7 @@ In the loader, you check first that these options are set before proceeding. The
After creating the client, you register it in the module's container using the container's `register` method. The method accepts two parameters:
1. The key to register the resource under, which in this case is `mongoClient`. You'll use this name later to resolve the client.
2. The resource to register in the container, which is the MongoDB client you created. However, you don't pass the resource as-is. Instead, you need to use an `asValue` function imported from the [awilix package](https://github.com/jeffijoe/awilix), which is the package used to implement the container functionality in Medusa.
2. The resource to register in the container, which is the MongoDB client you created. However, you don't pass the resource as-is. Instead, you need to use an `asValue` function imported from the [`@medusajs/framework/awilix` package](https://github.com/jeffijoe/awilix), which is the package used to implement the container functionality in Medusa.
### Use Custom Registered Resource in Module's Service

View File

@@ -195,8 +195,14 @@ npx medusa db:generate blog
The `db:generate` command of the Medusa CLI accepts one or more module names to generate the migration for. It will create a migration file for the Blog Module in the directory `src/modules/blog/migrations` similar to the following:
<Note>
As of [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0), MikroORM dependencies are included in the `@medusajs/framework` package. If you're using an older version of Medusa, change the import statement to `@mikro-orm/migrations`.
</Note>
```ts
import { Migration } from "@mikro-orm/migrations"
import { Migration } from "@medusajs/framework/@mikro-orm/migrations"
export class Migration20241121103722 extends Migration {

View File

@@ -70,17 +70,10 @@ A basic v2 project has the following dependencies in `package.json`:
"@medusajs/admin-sdk": "2.8.2",
"@medusajs/cli": "2.8.2",
"@medusajs/framework": "2.8.2",
"@medusajs/medusa": "2.8.2",
"@mikro-orm/core": "6.4.3",
"@mikro-orm/knex": "6.4.3",
"@mikro-orm/migrations": "6.4.3",
"@mikro-orm/postgresql": "6.4.3",
"awilix": "^8.0.1",
"pg": "^8.13.0"
"@medusajs/medusa": "2.8.2"
},
"devDependencies": {
"@medusajs/test-utils": "2.8.2",
"@mikro-orm/cli": "6.4.3",
"@swc/core": "1.5.7",
"@swc/jest": "^0.2.36",
"@types/jest": "^29.5.13",
@@ -107,25 +100,15 @@ The main changes are:
- `@medusajs/framework`
- `@medusajs/medusa`
- `@medusajs/test-utils` (as a dev dependency)
- You need to install the following extra packages:
- Database packages:
- `@mikro-orm/core@6.4.3`
- `@mikro-orm/knex@6.4.3`
- `@mikro-orm/migrations@6.4.3`
- `@mikro-orm/postgresql@6.4.3`
- `@mikro-orm/cli@6.4.3` (as a dev dependency)
- `pg^8.13.0`
- Framework packages:
- `awilix@^8.0.1`
- Development and Testing packages:
- `@swc/core@1.5.7`
- `@swc/jest@^0.2.36`
- `@types/node@^20.0.0`
- `jest@^29.7.0`
- `ts-node@^10.9.2`
- `typescript@^5.6.2`
- `vite@^5.2.11`
- `yalc@^1.0.0-pre.53`
- You need to install the following extra packages for development and testing:
- `@swc/core@1.5.7`
- `@swc/jest@^0.2.36`
- `@types/node@^20.0.0`
- `jest@^29.7.0`
- `ts-node@^10.9.2`
- `typescript@^5.6.2`
- `vite@^5.2.11`
- `yalc@^1.0.0-pre.53`
- Other packages, such as `@types/react` and `@types/react-dom`, are necessary for admin development and TypeScript support.
<Note>

View File

@@ -10,7 +10,14 @@ In this chapter, you'll learn about the different modes of running a Medusa inst
## What is Worker Mode?
By default, the Medusa application runs both the server, which handles all incoming requests, and the worker, which processes background tasks, in a single process. While this setup is suitable for development, it is not optimal for production environments where background tasks can be long-running or resource-intensive.
By default, the Medusa application runs in `shared` mode, which runs:
- `server`: the application server that handles incoming requests to the application's API routes.
- `worker`: the worker that processes background tasks. This includes scheduled jobs and subscribers.
While this setup is suitable for development, it is not optimal for production environments where background tasks can be long-running or resource-intensive.
### Worker Mode in Production
In a production environment, you should deploy two separate instances of your Medusa application:
@@ -104,3 +111,27 @@ ADMIN_DISABLED=true
</CodeTab>
</CodeTabs>
---
## Dividing Resources in Cluster Mode
<Note>
The `--servers` and `--workers` options were introduced in [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0).
</Note>
When running Medusa in [cluster mode](!resources!/medusa-cli/commands/start#starting-medusa-in-cluster-mode), you can specify the number or percentage of instances that are servers or workers by passing the `--servers` and `--workers` options:
```bash
npx medusa start --cluster 4 --servers 25% --workers 75% # Use 4 CPU cores, with 25% as servers and 75% as workers
npx medusa start --cluster 4 --servers 1 --workers 3 # Use 4 CPU cores, with 1 as server and 3 as workers
npx medusa start --cluster 4 --servers 1 --workers 1 # Use 4 CPU cores, with 1 as server and 1 as worker (the remaining 2 will run in shared mode)
```
In the above snippet you can see the following examples:
- In the first example, 25% of the instances (1 out of 4) will run as servers, and 75% (3 out of 4) will run as workers.
- In the second example, 1 instance will run as a server, and 3 instances will run as workers.
- In the third example, 1 instance will run as a server, and 1 instance will run as a worker. The remaining 2 instances will run in shared mode

View File

@@ -18,9 +18,9 @@ export const generatedEditDates = {
"app/learn/fundamentals/events-and-subscribers/page.mdx": "2025-10-16T09:36:04.864Z",
"app/learn/fundamentals/modules/container/page.mdx": "2025-07-31T14:24:04.087Z",
"app/learn/fundamentals/workflows/execute-another-workflow/page.mdx": "2025-08-01T07:28:51.036Z",
"app/learn/fundamentals/modules/loaders/page.mdx": "2025-06-16T13:34:16.462Z",
"app/learn/fundamentals/modules/loaders/page.mdx": "2025-10-09T11:41:31.724Z",
"app/learn/fundamentals/admin/widgets/page.mdx": "2025-07-25T15:08:07.035Z",
"app/learn/fundamentals/data-models/page.mdx": "2025-03-18T07:55:56.252Z",
"app/learn/fundamentals/data-models/page.mdx": "2025-10-09T11:39:30.944Z",
"app/learn/fundamentals/modules/remote-link/page.mdx": "2024-09-30T08:43:53.127Z",
"app/learn/fundamentals/api-routes/protected-routes/page.mdx": "2025-06-19T16:04:36.064Z",
"app/learn/fundamentals/workflows/add-workflow-hook/page.mdx": "2025-07-18T11:33:15.959Z",
@@ -32,7 +32,7 @@ export const generatedEditDates = {
"app/learn/fundamentals/admin/page.mdx": "2025-07-25T12:46:15.466Z",
"app/learn/fundamentals/workflows/long-running-workflow/page.mdx": "2025-08-01T07:16:21.736Z",
"app/learn/fundamentals/workflows/constructor-constraints/page.mdx": "2025-08-01T13:11:18.823Z",
"app/learn/fundamentals/data-models/write-migration/page.mdx": "2025-07-25T13:53:00.692Z",
"app/learn/fundamentals/data-models/write-migration/page.mdx": "2025-10-09T11:39:07.940Z",
"app/learn/fundamentals/data-models/manage-relationships/page.mdx": "2025-04-25T14:16:41.124Z",
"app/learn/fundamentals/modules/remote-query/page.mdx": "2024-07-21T21:20:24+02:00",
"app/learn/fundamentals/modules/options/page.mdx": "2025-03-18T15:12:34.510Z",
@@ -53,7 +53,7 @@ export const generatedEditDates = {
"app/learn/debugging-and-testing/testing-tools/integration-tests/api-routes/page.mdx": "2025-09-02T08:36:12.714Z",
"app/learn/debugging-and-testing/testing-tools/integration-tests/page.mdx": "2024-12-09T15:52:01.019Z",
"app/learn/debugging-and-testing/testing-tools/integration-tests/workflows/page.mdx": "2025-07-30T13:43:44.636Z",
"app/learn/debugging-and-testing/testing-tools/page.mdx": "2025-07-23T15:32:18.008Z",
"app/learn/debugging-and-testing/testing-tools/page.mdx": "2025-10-09T11:38:33.099Z",
"app/learn/debugging-and-testing/testing-tools/unit-tests/module-example/page.mdx": "2024-09-02T11:04:27.232Z",
"app/learn/debugging-and-testing/testing-tools/unit-tests/page.mdx": "2024-09-02T11:03:26.997Z",
"app/learn/fundamentals/modules/service-constraints/page.mdx": "2025-03-18T15:12:46.006Z",
@@ -66,10 +66,10 @@ export const generatedEditDates = {
"app/learn/fundamentals/module-links/directions/page.mdx": "2025-03-17T12:52:06.161Z",
"app/learn/fundamentals/module-links/page.mdx": "2025-04-17T08:50:17.036Z",
"app/learn/fundamentals/module-links/query/page.mdx": "2025-08-15T12:06:30.572Z",
"app/learn/fundamentals/modules/db-operations/page.mdx": "2025-04-25T14:26:25.000Z",
"app/learn/fundamentals/modules/db-operations/page.mdx": "2025-10-09T11:43:28.746Z",
"app/learn/fundamentals/modules/multiple-services/page.mdx": "2025-03-18T15:11:44.632Z",
"app/learn/fundamentals/modules/page.mdx": "2025-07-18T15:31:32.371Z",
"app/learn/debugging-and-testing/instrumentation/page.mdx": "2025-06-16T10:40:52.922Z",
"app/learn/fundamentals/modules/page.mdx": "2025-10-09T11:41:57.515Z",
"app/learn/debugging-and-testing/instrumentation/page.mdx": "2025-10-09T11:37:32.815Z",
"app/learn/fundamentals/api-routes/additional-data/page.mdx": "2025-04-17T08:50:17.036Z",
"app/learn/fundamentals/workflows/variable-manipulation/page.mdx": "2025-04-24T13:14:43.967Z",
"app/learn/customization/custom-features/api-route/page.mdx": "2025-10-16T11:23:11.195Z",
@@ -113,7 +113,7 @@ export const generatedEditDates = {
"app/learn/resources/usage/page.mdx": "2025-02-26T13:35:34.824Z",
"app/learn/configurations/medusa-config/page.mdx": "2025-09-30T06:04:15.705Z",
"app/learn/configurations/ts-aliases/page.mdx": "2025-07-23T15:32:18.008Z",
"app/learn/production/worker-mode/page.mdx": "2025-07-18T15:19:45.352Z",
"app/learn/production/worker-mode/page.mdx": "2025-10-13T10:33:27.403Z",
"app/learn/fundamentals/module-links/read-only/page.mdx": "2025-10-15T15:42:22.610Z",
"app/learn/fundamentals/data-models/properties/page.mdx": "2025-10-15T05:36:40.576Z",
"app/learn/fundamentals/framework/page.mdx": "2025-06-26T14:26:22.120Z",
@@ -125,12 +125,14 @@ export const generatedEditDates = {
"app/learn/introduction/build-with-llms-ai/page.mdx": "2025-10-02T15:10:49.394Z",
"app/learn/installation/docker/page.mdx": "2025-07-23T15:34:18.530Z",
"app/learn/fundamentals/generated-types/page.mdx": "2025-07-25T13:17:35.319Z",
"app/learn/introduction/from-v1-to-v2/page.mdx": "2025-09-01T06:34:41.179Z",
"app/learn/introduction/from-v1-to-v2/page.mdx": "2025-09-29T15:33:38.811Z",
"app/learn/debugging-and-testing/debug-workflows/page.mdx": "2025-07-30T13:45:14.117Z",
"app/learn/fundamentals/data-models/json-properties/page.mdx": "2025-07-31T14:25:01.268Z",
"app/learn/debugging-and-testing/logging/custom-logger/page.mdx": "2025-08-28T15:37:07.328Z",
"app/learn/fundamentals/scheduled-jobs/interval/page.mdx": "2025-09-02T08:36:12.714Z",
"app/learn/debugging-and-testing/feature-flags/create/page.mdx": "2025-09-02T08:36:12.714Z",
"app/learn/debugging-and-testing/feature-flags/page.mdx": "2025-09-02T08:36:12.714Z",
"app/learn/fundamentals/workflows/locks/page.mdx": "2025-09-15T09:37:00.808Z"
"app/learn/fundamentals/workflows/locks/page.mdx": "2025-09-15T09:37:00.808Z",
"app/learn/codemods/page.mdx": "2025-09-29T15:40:03.620Z",
"app/learn/codemods/replace-imports/page.mdx": "2025-10-09T11:37:44.754Z"
}

View File

@@ -1290,9 +1290,9 @@ export const generatedSidebars = [
"isPathHref": true,
"type": "link",
"path": "/learn/production/worker-mode",
"title": "Worker Mode",
"title": "Worker Modes",
"children": [],
"chapterTitle": "8.2. Worker Mode",
"chapterTitle": "8.2. Worker Modes",
"number": "8.2."
},
{
@@ -1345,6 +1345,27 @@ export const generatedSidebars = [
"children": [],
"chapterTitle": "9.2. Release Notes",
"number": "9.2."
},
{
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/learn/codemods",
"title": "Codemods",
"children": [
{
"loaded": true,
"isPathHref": true,
"type": "link",
"title": "Replace Imports (v2.11.0+)",
"path": "/learn/codemods/replace-imports",
"children": [],
"chapterTitle": "9.3.1. Replace Imports (v2.11.0+)",
"number": "9.3.1."
}
],
"chapterTitle": "9.3. Codemods",
"number": "9.3."
}
],
"chapterTitle": "9. Upgrade",

View File

@@ -139,6 +139,294 @@ npx medusa build --admin-only
The next chapter covers how to deploy the production build.
# Medusa Codemods
In this chapter, you'll learn about Medusa codemods and the list of available codemods.
## What are Codemods?
Codemods are scripts that help you automate codebase changes. They are especially useful when updating to a new version that requires large changes to your codebase.
Medusa provides codemods to help you update your codebase. Use these codemods when updating to their respective versions.
***
## List of Medusa Codemods
# Replace Imports Codemod (v2.11.0+)
In this chapter, you'll learn about the codemod that helps you replace imports in your codebase when upgrading to Medusa v2.11.0.
## What is the Replace Imports Codemod?
[Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0) optimized the package structure by consolidating several external packages into the `@medusajs/framework` package.
Previously, you had to install and manage packages related to MikroORM, Awilix, OpenTelemetry, and the `pg` package separately in your Medusa application. Starting with v2.11.0, these packages are included in the `@medusajs/framework` package.
For example, instead of importing `@mikro-orm/core`, you now import it from `@medusajs/framework/mikro-orm/core`. This applies to all of the following packages:
- `@mikro-orm/*` packages (for example, `@mikro-orm/core`, `@mikro-orm/migrations`, etc.) -> `@medusajs/framework/mikro-orm/{subpath}`
- `awilix` -> `@medusajs/framework/awilix`
- `pg` -> `@medusajs/framework/pg`
- `@opentelemetry/instrumentation-pg` -> `@medusajs/framework/opentelemetry/instrumentation-pg`
- `@opentelemetry/resources` -> `@medusajs/framework/opentelemetry/resources`
- `@opentelemetry/sdk-node` -> `@medusajs/framework/opentelemetry/sdk-node`
- `@opentelemetry/sdk-trace-node` -> `@medusajs/framework/opentelemetry/sdk-trace-node`
To help you update your codebase to reflect these changes, Medusa provides a codemod that automatically replaces imports of these packages throughout your codebase.
***
## Using the Replace Imports Codemod
To use the replace imports codemod, create the file `replace-imports.js` in the root of your Medusa application with the following content:
```js
#!/usr/bin/env node
const fs = require("fs")
const path = require("path")
const { execSync } = require("child_process")
/**
- Script to replace imports and require statements from mikro-orm/{subpath}, awilix, and pg
- to their @medusajs/framework equivalents
*/
// Define the replacement mappings
const replacements = [
// MikroORM imports - replace mikro-orm/{subpath} with @medusajs/framework/mikro-orm/{subpath}
{
pattern: /from\s+['"]@?mikro-orm\/([^'"]+)['"]/g,
// eslint-disable-next-line quotes
replacement: 'from "@medusajs/framework/mikro-orm/$1"',
},
// Awilix imports - replace awilix with @medusajs/framework/awilix
{
pattern: /from\s+['"]awilix['"]/g,
// eslint-disable-next-line quotes
replacement: 'from "@medusajs/framework/awilix"',
},
// PG imports - replace pg with @medusajs/framework/pg
{
pattern: /from\s+['"]pg['"]/g,
// eslint-disable-next-line quotes
replacement: 'from "@medusajs/framework/pg"',
},
// OpenTelemetry imports - replace @opentelemetry/instrumentation-pg, @opentelemetry/resources,
// @opentelemetry/sdk-node, and @opentelemetry/sdk-trace-node with @medusajs/framework/opentelemetry/{subpath}
{
pattern: /from\s+['"]@?opentelemetry\/(instrumentation-pg|resources|sdk-node|sdk-trace-node)['"]/g,
// eslint-disable-next-line quotes
replacement: 'from "@medusajs/framework/opentelemetry/$1"',
},
// MikroORM require statements - replace require('@?mikro-orm/{subpath}') with require('@medusajs/framework/mikro-orm/{subpath}')
{
pattern: /require\s*\(\s*['"]@?mikro-orm\/([^'"]+)['"]\s*\)/g,
// eslint-disable-next-line quotes
replacement: 'require("@medusajs/framework/mikro-orm/$1")',
},
// Awilix require statements - replace require('awilix') with require('@medusajs/framework/awilix')
{
pattern: /require\s*\(\s*['"]awilix['"]\s*\)/g,
// eslint-disable-next-line quotes
replacement: 'require("@medusajs/framework/awilix")',
},
// PG require statements - replace require('pg') with require('@medusajs/framework/pg')
{
pattern: /require\s*\(\s*['"]pg['"]\s*\)/g,
// eslint-disable-next-line quotes
replacement: 'require("@medusajs/framework/pg")',
},
// OpenTelemetry require statements - replace require('@opentelemetry/instrumentation-pg'),
// require('@opentelemetry/resources'), require('@opentelemetry/sdk-node'), and
// require('@opentelemetry/sdk-trace-node') with require('@medusajs/framework/opentelemetry/{subpath}')
{
pattern: /require\s*\(\s*['"]@?opentelemetry\/(instrumentation-pg|resources|sdk-node|sdk-trace-node)['"]\s*\)/g,
// eslint-disable-next-line quotes
replacement: 'require("@medusajs/framework/opentelemetry/$1")',
},
]
function processFile(filePath) {
try {
const content = fs.readFileSync(filePath, "utf8")
let modifiedContent = content
let wasModified = false
replacements.forEach(({ pattern, replacement }) => {
const newContent = modifiedContent.replace(pattern, replacement)
if (newContent !== modifiedContent) {
wasModified = true
modifiedContent = newContent
}
})
if (wasModified) {
fs.writeFileSync(filePath, modifiedContent)
console.log(`✓ Updated: ${filePath}`)
return true
}
return false
} catch (error) {
console.error(`✗ Error processing ${filePath}:`, error.message)
return false
}
}
function getTargetFiles() {
try {
// Get the current script's filename to exclude it from processing
const currentScript = path.basename(__filename)
// Find TypeScript/JavaScript files, excluding common directories that typically don't contain target imports
const findCommand = `find . -name node_modules -prune -o -name .git -prune -o -name dist -prune -o -name build -prune -o -name coverage -prune -o -name "*.ts" -print -o -name "*.js" -print -o -name "*.tsx" -print -o -name "*.jsx" -print`
const files = execSync(findCommand, {
encoding: "utf8",
maxBuffer: 50 * 1024 * 1024, // 50MB buffer
})
.split("\n")
.filter((line) => line.trim())
console.log(files)
const targetFiles = []
let processedCount = 0
console.log(`📄 Scanning ${files.length} files for target imports and require statements...`)
for (const file of files) {
try {
// Skip the current script file
const fileName = path.basename(file)
if (fileName === currentScript) {
processedCount++
continue
}
const content = fs.readFileSync(file, "utf8")
if (
/from\s+['"]@?mikro-orm\//.test(content) ||
/from\s+['"]awilix['"]/.test(content) ||
/from\s+['"]pg['"]/.test(content) ||
/require\s*\(\s*['"]@?mikro-orm\//.test(content) ||
/require\s*\(\s*['"]awilix['"]/.test(content) ||
/require\s*\(\s*['"]pg['"]/.test(content)
) {
targetFiles.push(file.startsWith("./") ? file.slice(2) : file)
}
processedCount++
if (processedCount % 100 === 0) {
process.stdout.write(
`\r📄 Processed ${processedCount}/${files.length} files...`
)
}
} catch (fileError) {
// Skip files that can't be read
continue
}
}
if (processedCount > 0) {
console.log(`\r📄 Processed ${processedCount} files. `)
}
return targetFiles
} catch (error) {
console.error("Error finding target files:", error.message)
return []
}
}
function main() {
console.log("🔄 Finding files with target imports and require statements...")
const targetFiles = getTargetFiles()
if (targetFiles.length === 0) {
console.log(" No files found with target imports or require statements.")
return
}
console.log(`📁 Found ${targetFiles.length} files to process`)
let modifiedCount = 0
let errorCount = 0
targetFiles.forEach((filePath) => {
const fullPath = path.resolve(filePath)
if (fs.existsSync(fullPath)) {
if (processFile(fullPath)) {
modifiedCount++
}
} else {
console.warn(`⚠️ File not found: ${filePath}`)
errorCount++
}
})
console.log("\n📊 Summary:")
console.log(` Files processed: ${targetFiles.length}`)
console.log(` Files modified: ${modifiedCount}`)
console.log(` Errors: ${errorCount}`)
if (modifiedCount > 0) {
console.log("\n✅ Import replacement completed successfully!")
console.log("\n💡 Next steps:")
console.log(" 1. Review the changes with: git diff")
console.log(" 2. Run your tests to ensure everything works correctly")
console.log(" 3. Commit the changes if you're satisfied")
} else {
console.log(
"\n✅ No modifications needed - all imports are already correct!"
)
}
}
// Run if called directly
if (require.main === module) {
main()
}
module.exports = { processFile, getTargetFiles, main }
```
This script scans your project for files that import from `mikro-orm/{subpath}`, `awilix`, or `pg`, and replaces those imports with their new equivalents from `@medusajs/framework`. It handles both ES module `import` statements and CommonJS `require` statements in JavaScript and TypeScript files.
Next, run the following command in your terminal to make the script executable:
You can run the script using `node` without changing permissions.
```bash
chmod +x replace-imports.js
```
Finally, execute the script with the following command:
```bash
node replace-imports.js
```
This will scan your project files, apply the necessary import replacements, and provide a summary of the changes made.
***
## Next Steps
After running the codemod, review the changes made to your codebase. You can use `git diff` to see the modifications. Additionally, run your tests to ensure everything works as expected.
If everything is working correctly, you can remove the `replace-imports.js` file from your project. You can also remove the following packages from your `package.json`, as they're now included in the `@medusajs/framework` package:
- `@mikro-orm/*` packages (for example, `@mikro-orm/core`, `@mikro-orm/migrations`, etc.)
- `awilix`
- `pg`
- `@opentelemetry/instrumentation-pg`
- `@opentelemetry/resources`
- `@opentelemetry/sdk-node`
- `@opentelemetry/sdk-trace-node`
# Medusa Application Configuration
In this chapter, you'll learn available configurations in the Medusa application. You can change the application's configurations to customize the behavior of the application, its integrated modules and plugins, and more.
@@ -4270,13 +4558,9 @@ Medusa uses [OpenTelemetry](https://opentelemetry.io/) for instrumentation and r
### Install Dependencies
Start by installing the following OpenTelemetry dependencies in your Medusa project:
As of [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0), OpenTelemetry dependencies are installed by default in new Medusa projects. If you're using an older version of Medusa, you need to install the `@opentelemetry/sdk-node`, `@opentelemetry/resources`, `@opentelemetry/sdk-trace-node`, and `@opentelemetry/instrumentation-pg` dependencies.
```bash npm2yarn
npm install @opentelemetry/sdk-node @opentelemetry/resources @opentelemetry/sdk-trace-node @opentelemetry/instrumentation-pg
```
Also, install the dependencies relevant for the exporter you use. If you're using Zipkin, install the following dependencies:
Before you start, you must install the dependencies relevant for the exporter you use. If you're using Zipkin, install the following dependencies:
```bash npm2yarn
npm install @opentelemetry/exporter-zipkin
@@ -6080,8 +6364,10 @@ if (process.env.TEST_TYPE === "integration:http") {
Next, create the `integration-tests/setup.js` file with the following content:
As of [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0), MikroORM dependencies are included in the `@medusajs/framework` package. If you're using an older version of Medusa, change the require statement to `@mikro-orm/core`.
```js title="integration-tests/setup.js"
const { MetadataStorage } = require("@mikro-orm/core")
const { MetadataStorage } = require("@medusajs/framework/@mikro-orm/core")
MetadataStorage.clear()
```
@@ -6652,9 +6938,44 @@ For example, the `VITE_MY_API_KEY` environment variable in the example above wil
## Environment Variables in Plugins
As explained in the [previous section](#environment-variables-in-production), environment variables are inlined into the build. This presents a limitation for plugins, where you can't use environment variables.
Environment variable support in plugins is available starting [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0). Refer to the [Medusa versions prior to v2.11.0](#for-medusa-versions-prior-to-v2110) section for more details if you're using an earlier version.
Instead, only the following global variable is available in plugins:
For plugins, you can use environment variables without a prefix. Then, Medusa applications that use the plugin can set the environment variable with the `PLUGIN_` prefix.
For example, you can create a widget in your plugin that uses the `MY_API_KEY` environment variable:
```tsx highlights={[["8"]]}
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container, Heading } from "@medusajs/ui"
const ProductWidget = () => {
return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">API Key: {import.meta.env.MY_API_KEY}</Heading>
</div>
</Container>
)
}
export const config = defineWidgetConfig({
zone: "product.details.before",
})
export default ProductWidget
```
Then, in the Medusa application that uses the plugin, set the environment variable with the `PLUGIN_` prefix:
```bash
PLUGIN_MY_API_KEY=sk_123
```
The `MY_API_KEY` environment variable in the plugin will be replaced with the value of `PLUGIN_MY_API_KEY` during the build process of the Medusa application.
### Global Variables in Plugins
Plugins also have the following global variables available:
- `__BACKEND_URL__`: The URL of the Medusa backend, as set in the [Medusa configurations](https://docs.medusajs.com/learn/configurations/medusa-config#backendurl/index.html.md).
- `__BASE__`: The base path of the Medusa Admin. (For example, `/app`).
@@ -6693,6 +7014,14 @@ declare const __BASE__: string
declare const __STOREFRONT_URL__: string
```
### For Medusa versions prior to v2.11.0
### Instructions for Medusa versions prior to v2.11.0
As explained in the [Environment Variables in Production section](#environment-variables-in-production), environment variables are inlined into the build. This presents a limitation for plugins, where you can't use environment variables.
Instead, you can use the [Plugin Global Variables](#global-variables-in-plugins) described above to access the backend URL, base path, and storefront URL.
# Admin Development
@@ -10779,8 +11108,10 @@ npx medusa db:generate blog
The `db:generate` command of the Medusa CLI accepts one or more module names to generate the migration for. It will create a migration file for the Blog Module in the directory `src/modules/blog/migrations` similar to the following:
As of [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0), MikroORM dependencies are included in the `@medusajs/framework` package. If you're using an older version of Medusa, change the import statement to `@mikro-orm/migrations`.
```ts
import { Migration } from "@mikro-orm/migrations"
import { Migration } from "@medusajs/framework/@mikro-orm/migrations"
export class Migration20241121103722 extends Migration {
@@ -11548,8 +11879,10 @@ You can also write migrations manually. To do that, create a file in the `migrat
For example:
As of [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0), MikroORM dependencies are included in the `@medusajs/framework` package. If you're using an older version of Medusa, change the import statement to `@mikro-orm/migrations`.
```ts title="src/modules/blog/migrations/Migration202507021059_create_author.ts"
import { Migration } from "@mikro-orm/migrations"
import { Migration } from "@medusajs/framework/@mikro-orm/migrations"
export class Migration202507021059 extends Migration {
@@ -11564,7 +11897,7 @@ export class Migration202507021059 extends Migration {
}
```
The migration class in the file extends the `Migration` class imported from `@mikro-orm/migrations`. In the `up` and `down` method of the migration class, you use the `addSql` method provided by MikroORM's `Migration` class to run PostgreSQL syntax.
The migration class in the file extends the `Migration` class imported from `@medusajs/framework/@mikro-orm/migrations`. In the `up` and `down` method of the migration class, you use the `addSql` method provided by MikroORM's `Migration` class to run PostgreSQL syntax.
In the example above, the `up` method creates the table `author`, and the `down` method drops the table if the migration is reverted.
@@ -15881,6 +16214,8 @@ So, to run database queries in a service:
For example, in your service, add the following methods:
As of [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0), MikroORM dependencies are included in the `@medusajs/framework` package. If you're using an older version of Medusa, change the import statement to `@mikro-orm/knex`.
```ts highlights={methodsHighlight}
// other imports...
import {
@@ -15888,7 +16223,7 @@ import {
MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
class BlogModuleService {
// ...
@@ -15915,7 +16250,7 @@ class BlogModuleService {
You add two methods `getCount` and `getCountSql` that have the `InjectManager` decorator. Each of the methods also accept the `sharedContext` parameter which has the `MedusaContext` decorator.
The entity manager is injected to the `sharedContext.manager` property, which is an instance of [EntityManager from the @mikro-orm/knex package](https://mikro-orm.io/api/knex/class/EntityManager).
The entity manager is injected to the `sharedContext.manager` property, which is an instance of [EntityManager from the `@medusajs/framework/@mikro-orm/knex` package](https://mikro-orm.io/api/knex/class/EntityManager).
You use the manager in the `getCount` method to retrieve the number of records in a table, and in the `getCountSql` to run a PostgreSQL query that retrieves the count.
@@ -15952,7 +16287,7 @@ import {
MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
class BlogModuleService {
// ...
@@ -16009,7 +16344,7 @@ import {
MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
class BlogModuleService {
// ...
@@ -16052,7 +16387,7 @@ import {
MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
class BlogModuleService {
// ...
@@ -16185,7 +16520,7 @@ import {
MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
class BlogModuleService {
// ...
@@ -16300,7 +16635,7 @@ import {
MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
class BlogModuleService {
// ...
@@ -16366,7 +16701,7 @@ The second parameter of the `baseRepository_.transaction` method is an object of
```ts highlights={[["24"]]}
// other imports...
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
import {
InjectTransactionManager,
MedusaContext,
@@ -16409,8 +16744,8 @@ import {
MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { IsolationLevel } from "@mikro-orm/core"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
import { IsolationLevel } from "@medusajs/framework/@mikro-orm/core"
class BlogModuleService {
// ...
@@ -16444,7 +16779,7 @@ import {
MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
class BlogModuleService {
// ...
@@ -16883,9 +17218,11 @@ Consider your have a MongoDB module that allows you to perform operations on a M
To connect to the database, you create the following loader in your module:
As of [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0), Awilix dependencies are included in the `@medusajs/framework` package. If you're using an older version of Medusa, change the import statement to `awilix`.
```ts title="src/modules/mongo/loaders/connection.ts" highlights={loaderHighlights}
import { LoaderOptions } from "@medusajs/framework/types"
import { asValue } from "awilix"
import { asValue } from "@medusajs/framework/awilix"
import { MongoClient } from "mongodb"
type ModuleOptions = {
@@ -16951,7 +17288,7 @@ In the loader, you check first that these options are set before proceeding. The
After creating the client, you register it in the module's container using the container's `register` method. The method accepts two parameters:
1. The key to register the resource under, which in this case is `mongoClient`. You'll use this name later to resolve the client.
2. The resource to register in the container, which is the MongoDB client you created. However, you don't pass the resource as-is. Instead, you need to use an `asValue` function imported from the [awilix package](https://github.com/jeffijoe/awilix), which is the package used to implement the container functionality in Medusa.
2. The resource to register in the container, which is the MongoDB client you created. However, you don't pass the resource as-is. Instead, you need to use an `asValue` function imported from the [`@medusajs/framework/awilix` package](https://github.com/jeffijoe/awilix), which is the package used to implement the container functionality in Medusa.
### Use Custom Registered Resource in Module's Service
@@ -17503,8 +17840,10 @@ npx medusa db:generate blog
The `db:generate` command of the Medusa CLI accepts one or more module names to generate the migration for. It will create a migration file for the Blog Module in the directory `src/modules/blog/migrations` similar to the following:
As of [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0), MikroORM dependencies are included in the `@medusajs/framework` package. If you're using an older version of Medusa, change the import statement to `@mikro-orm/migrations`.
```ts
import { Migration } from "@mikro-orm/migrations"
import { Migration } from "@medusajs/framework/@mikro-orm/migrations"
export class Migration20241121103722 extends Migration {
@@ -22248,17 +22587,10 @@ A basic v2 project has the following dependencies in `package.json`:
"@medusajs/admin-sdk": "2.8.2",
"@medusajs/cli": "2.8.2",
"@medusajs/framework": "2.8.2",
"@medusajs/medusa": "2.8.2",
"@mikro-orm/core": "6.4.3",
"@mikro-orm/knex": "6.4.3",
"@mikro-orm/migrations": "6.4.3",
"@mikro-orm/postgresql": "6.4.3",
"awilix": "^8.0.1",
"pg": "^8.13.0"
"@medusajs/medusa": "2.8.2"
},
"devDependencies": {
"@medusajs/test-utils": "2.8.2",
"@mikro-orm/cli": "6.4.3",
"@swc/core": "1.5.7",
"@swc/jest": "^0.2.36",
"@types/jest": "^29.5.13",
@@ -22285,25 +22617,15 @@ The main changes are:
- `@medusajs/framework`
- `@medusajs/medusa`
- `@medusajs/test-utils` (as a dev dependency)
- You need to install the following extra packages:
- Database packages:
- `@mikro-orm/core@6.4.3`
- `@mikro-orm/knex@6.4.3`
- `@mikro-orm/migrations@6.4.3`
- `@mikro-orm/postgresql@6.4.3`
- `@mikro-orm/cli@6.4.3` (as a dev dependency)
- `pg^8.13.0`
- Framework packages:
- `awilix@^8.0.1`
- Development and Testing packages:
- `@swc/core@1.5.7`
- `@swc/jest@^0.2.36`
- `@types/node@^20.0.0`
- `jest@^29.7.0`
- `ts-node@^10.9.2`
- `typescript@^5.6.2`
- `vite@^5.2.11`
- `yalc@^1.0.0-pre.53`
- You need to install the following extra packages for development and testing:
- `@swc/core@1.5.7`
- `@swc/jest@^0.2.36`
- `@types/node@^20.0.0`
- `jest@^29.7.0`
- `ts-node@^10.9.2`
- `typescript@^5.6.2`
- `vite@^5.2.11`
- `yalc@^1.0.0-pre.53`
- Other packages, such as `@types/react` and `@types/react-dom`, are necessary for admin development and TypeScript support.
Notice that Medusa now uses MikroORM instead of TypeORM for database functionalities.
@@ -23629,7 +23951,14 @@ In this chapter, you'll learn about the different modes of running a Medusa inst
## What is Worker Mode?
By default, the Medusa application runs both the server, which handles all incoming requests, and the worker, which processes background tasks, in a single process. While this setup is suitable for development, it is not optimal for production environments where background tasks can be long-running or resource-intensive.
By default, the Medusa application runs in `shared` mode, which runs:
- `server`: the application server that handles incoming requests to the application's API routes.
- `worker`: the worker that processes background tasks. This includes scheduled jobs and subscribers.
While this setup is suitable for development, it is not optimal for production environments where background tasks can be long-running or resource-intensive.
### Worker Mode in Production
In a production environment, you should deploy two separate instances of your Medusa application:
@@ -23714,6 +24043,26 @@ ADMIN_DISABLED=false
ADMIN_DISABLED=true
```
***
## Dividing Resources in Cluster Mode
The `--servers` and `--workers` options were introduced in [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0).
When running Medusa in [cluster mode](https://docs.medusajs.com/resources/medusa-cli/commands/start#starting-medusa-in-cluster-mode/index.html.md), you can specify the number or percentage of instances that are servers or workers by passing the `--servers` and `--workers` options:
```bash
npx medusa start --cluster 4 --servers 25% --workers 75% # Use 4 CPU cores, with 25% as servers and 75% as workers
npx medusa start --cluster 4 --servers 1 --workers 3 # Use 4 CPU cores, with 1 as server and 3 as workers
npx medusa start --cluster 4 --servers 1 --workers 1 # Use 4 CPU cores, with 1 as server and 1 as worker (the remaining 2 will run in shared mode)
```
In the above snippet you can see the following examples:
- In the first example, 25% of the instances (1 out of 4) will run as servers, and 75% (3 out of 4) will run as workers.
- In the second example, 1 instance will run as a server, and 3 instances will run as workers.
- In the third example, 1 instance will run as a server, and 1 instance will run as a worker. The remaining 2 instances will run in shared mode
# Translate Medusa Admin
@@ -36070,34 +36419,75 @@ Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/pro
## What is a Campaign?
A [Campaign](https://docs.medusajs.com/references/promotion/models/Campaign/index.html.md) combines [promotions](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/concepts#what-is-a-promotion/index.html.md) under the same conditions, such as start and end dates.
A [Campaign](https://docs.medusajs.com/references/promotion/models/Campaign/index.html.md) groups [promotions](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/concepts#what-is-a-promotion/index.html.md) under the same conditions, such as start and end dates.
Campaigns are useful for grouping promotions that share the same time frame or target audience. They're also useful for limiting the usage of promotions.
Use campaigns to group promotions that share the same time frame or target audience, and to limit promotion usage.
![A diagram showcasing the relation between the Campaign and Promotion data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709899225/Medusa%20Resources/campagin-promotion_hh3qsi.jpg)
***
## Campaign Limits
## Limit Promotion Usage with Campaign Budgets
Each campaign can have a budget represented by the [CampaignBudget data model](https://docs.medusajs.com/references/promotion/models/CampaignBudget/index.html.md). The budget limits how many times the promotion can be used.
Each campaign can have a budget represented by the [CampaignBudget data model](https://docs.medusajs.com/references/promotion/models/CampaignBudget/index.html.md). The budget limits how many times a promotion can be used.
There are two types of budgets:
There are three types of budgets: two that are global and one that is based on cart attributes.
- `spend`: An amount that, when crossed, the promotion becomes unusable.
- For example, if the amount limit is set to `$100`, and the total amount of usage of this promotion crosses that threshold, the promotion can no longer be applied.
- `usage`: The number of times that a promotion can be used.
- For example, if the usage limit is set to `10`, the promotion can be used only 10 times by customers. After that, it can no longer be applied.
### Global Budgets
A global budget limits promotion usage without considering any cart attributes.
There are two types of global budgets:
- `spend`: An amount that, when exceeded, makes the promotion unusable.
- For example, if the amount limit is `$100` and the total usage of this promotion exceeds that threshold, the promotion can no longer be applied.
- `usage`: The number of times a promotion can be used.
- For example, if the usage limit is `10`, customers can use the promotion only 10 times. After that, it can no longer be applied.
![A diagram showcasing the relation between the Campaign and CampaignBudget data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709899463/Medusa%20Resources/campagin-budget_rvqlmi.jpg)
### How Budgets Limit Promotion Usage
Global budgets track usage and limits through the following properties of the `CampaignBudget` data model:
When a customer tries to use a promotion, Medusa checks whether the campaign has a budget and if the budget limit has been reached. If so, the promotion cannot be applied.
- `limit`: The maximum amount or number of uses allowed for the promotion.
- `used`: The current amount spent or number of times the promotion has been used.
For example, if a campaign has a budget of type `usage` with a limit of `10`, and the promotion has already been used 10 times, it cannot be applied anymore and is considered expired.
### Attribute-based Budgets
However, once a promotion is applied to a cart, it remains valid until the order is completed, even if the budget limit is reached in the meantime. This ensures that customers who have already applied the promotion can still benefit from it during checkout.
Attribute-based budgets were introduced in [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0).
An attribute-based budget limits promotion usage based on a cart attribute. Use these budget types to have granular control over how many times a promotion can be used based on specific attributes.
There's one type of attribute-based budget, which is `use_by_attribute`. It allows you to limit the number of times a promotion can be used based on a specific cart attribute.
#### Allowed Attributes
There are two attributes that you can limit promotion usage by:
- `customer_id`: Limits promotion usage based on the unique identifier of a customer.
- `customer_email`: Limits promotion usage based on the email address of a customer.
These attributes are compared against the cart's `customer_id` or `email` to determine how many times the promotion has been used for that specific attribute value, and whether the budget limit has been reached.
#### Tracking Attribute-based Usage
The `CampaignBudgetUsage` data model tracks the usage of attribute-based budgets. It tracks how many times a promotion has been used for each unique attribute value. It includes the following properties:
1. `attribute_value`: The value of the attribute, such as a specific customer ID or email.
2. `used`: The number of times the promotion has been used for that attribute value.
For example, if the attribute is `customer_id`, a new `CampaignBudgetUsage` record is created for each customer that uses the promotion to track their individual usage. Once a customer exceeds the limit set in the `CampaignBudget`, they can no longer use the promotion.
![A diagram showcasing the relation between the CampaignBudget and CampaignBudgetUsage data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1760340527/Medusa%20Resources/campaign-budget-attr_fv0v2u.jpg)
***
## How Campaign Budgets Limit Promotion Usage
When a customer tries to use a promotion, Medusa checks whether the campaign has a budget and if the budget limit has been reached. If the limit is reached, the promotion cannot be applied.
For example, if a campaign has a `usage` budget with a limit of `10` and the promotion has already been used 10 times, it can no longer be applied and is considered expired.
However, once a promotion is applied to a cart, it remains valid until the order is completed, even if the budget limit is reached in the meantime. This ensures that customers who already applied the promotion can still benefit from it during checkout.
# Promotion Concepts
@@ -44653,7 +45043,46 @@ npx medusa start
|---|---|---|---|---|
|\`-H \<host>\`|Set host of the Medusa server.|\`localhost\`|
|\`-p \<port>\`|Set port of the Medusa server.|\`9000\`|
|\`--cluster \<number>\`|Start Medusa's Node.js server in |Cluster mode is disabled by default. If the option is passed but no number is passed, Medusa will try to consume all available CPU cores.|
|\`--cluster \<string> \[--workers \<string>] \[--servers \<string>]\`|Start Medusa in cluster mode. Learn more in the |Cluster mode is disabled by default. If the option is passed but no number or percentage is passed, Medusa will try to consume all available CPU cores.|
***
## Starting Medusa in Cluster Mode
Prior to [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0), the `--cluster` option accepted a number value only. You can now pass either a number or a percentage value, and you can also specify the number of servers and workers.
Medusa supports starting the Node.js server in [cluster mode](https://expressjs.com/en/advanced/best-practice-performance.html#run-your-app-in-a-cluster), which significantly improves performance as the workload and tasks are distributed among all available instances instead of a single one.
Cluster mode is disabled by default. To enable it, pass the `--cluster` option when starting Medusa:
```bash
npx medusa start --cluster
```
When the `--cluster` option is passed without a number or percentage value, Medusa will try to consume all available CPU cores.
### Specify Number or Percentage of CPU Cores
You can specify the number or percentage of CPU cores to be used by passing a number or percentage value to the `--cluster` option:
```bash
npx medusa start --cluster 2 # Use 2 CPU cores
npx medusa start --cluster 50% # Use 50% of available CPU
```
### Specify Number of Servers and Workers
When running Medusa in cluster mode, you can specify the number or percentage of instances that are [servers or workers](https://docs.medusajs.com/docs/learn/production/worker-mode/index.html.md) by passing the `--servers` and `--workers` options:
```bash
npx medusa start --cluster 4 --servers 25% --workers 75% # Use 4 CPU cores, with 25% as servers and 75% as workers
npx medusa start --cluster 4 --servers 1 --workers 3 # Use 4 CPU cores, with 1 as server and 3 as workers
npx medusa start --cluster 4 --servers 1 --workers 1 # Use 4 CPU cores, with 1 as server and 1 as worker (the remaining 2 will run in shared mode)
```
When the number or percentage of servers and workers don't add up to the total number of instances in cluster mode, the remaining instances will run in shared mode.
Learn more in the [Worker Mode](https://docs.medusajs.com/docs/learn/production/worker-mode/index.html.md) guide.
# telemetry Command - Medusa CLI Reference
@@ -45020,7 +45449,46 @@ npx medusa start
|---|---|---|---|---|
|\`-H \<host>\`|Set host of the Medusa server.|\`localhost\`|
|\`-p \<port>\`|Set port of the Medusa server.|\`9000\`|
|\`--cluster \<number>\`|Start Medusa's Node.js server in |Cluster mode is disabled by default. If the option is passed but no number is passed, Medusa will try to consume all available CPU cores.|
|\`--cluster \<string> \[--workers \<string>] \[--servers \<string>]\`|Start Medusa in cluster mode. Learn more in the |Cluster mode is disabled by default. If the option is passed but no number or percentage is passed, Medusa will try to consume all available CPU cores.|
***
## Starting Medusa in Cluster Mode
Prior to [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0), the `--cluster` option accepted a number value only. You can now pass either a number or a percentage value, and you can also specify the number of servers and workers.
Medusa supports starting the Node.js server in [cluster mode](https://expressjs.com/en/advanced/best-practice-performance.html#run-your-app-in-a-cluster), which significantly improves performance as the workload and tasks are distributed among all available instances instead of a single one.
Cluster mode is disabled by default. To enable it, pass the `--cluster` option when starting Medusa:
```bash
npx medusa start --cluster
```
When the `--cluster` option is passed without a number or percentage value, Medusa will try to consume all available CPU cores.
### Specify Number or Percentage of CPU Cores
You can specify the number or percentage of CPU cores to be used by passing a number or percentage value to the `--cluster` option:
```bash
npx medusa start --cluster 2 # Use 2 CPU cores
npx medusa start --cluster 50% # Use 50% of available CPU
```
### Specify Number of Servers and Workers
When running Medusa in cluster mode, you can specify the number or percentage of instances that are [servers or workers](https://docs.medusajs.com/docs/learn/production/worker-mode/index.html.md) by passing the `--servers` and `--workers` options:
```bash
npx medusa start --cluster 4 --servers 25% --workers 75% # Use 4 CPU cores, with 25% as servers and 75% as workers
npx medusa start --cluster 4 --servers 1 --workers 3 # Use 4 CPU cores, with 1 as server and 3 as workers
npx medusa start --cluster 4 --servers 1 --workers 1 # Use 4 CPU cores, with 1 as server and 1 as worker (the remaining 2 will run in shared mode)
```
When the number or percentage of servers and workers don't add up to the total number of instances in cluster mode, the remaining instances will run in shared mode.
Learn more in the [Worker Mode](https://docs.medusajs.com/docs/learn/production/worker-mode/index.html.md) guide.
# telemetry Command - Medusa CLI Reference
@@ -52113,7 +52581,7 @@ import {
MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
class BlogModuleService {
// ...
@@ -53418,7 +53886,7 @@ To register a resource in the Module's container using a loader, use the `contai
import {
LoaderOptions,
} from "@medusajs/framework/types"
import { asValue } from "awilix"
import { asValue } from "@medusajs/framework/awilix"
export default async function helloWorldLoader({
container,
@@ -76325,7 +76793,7 @@ In `src/modules/product-review/service.ts`, add the following methods to the `Pr
import { InjectManager, MedusaService, MedusaContext } from "@medusajs/framework/utils"
import Review from "./models/review"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
class ProductReviewModuleService extends MedusaService({
Review,
@@ -79587,7 +80055,7 @@ Loaders are created in a TypeScript or JavaScript file under the `loaders` direc
```ts title="src/modules/contentful/loader/create-content-models.ts" highlights={loaderHighlights}
import { LoaderOptions } from "@medusajs/framework/types"
import { asValue } from "awilix"
import { asValue } from "@medusajs/framework/awilix"
import { createClient } from "contentful-management"
import { MedusaError } from "@medusajs/framework/utils"
@@ -92854,15 +93322,7 @@ Refer to the [Instrumentation](https://docs.medusajs.com/docs/learn/debugging-an
### a. Install Instrumentation Dependencies
To set up instrumentation in Medusa, you need to install the necessary OpenTelemetry dependencies.
In your Medusa application's directory, run the following command:
```bash npm2yarn
npm install @opentelemetry/sdk-node @opentelemetry/resources @opentelemetry/sdk-trace-node @opentelemetry/instrumentation-pg
```
Then, you need to install the dependencies necessary for the monitoring tool you want to use, which is Sentry in this case.
To set up instrumentation in Medusa, you need to install the dependencies necessary for the monitoring tool you want to use, which is Sentry in this case.
So, run the following command to install the necessary Sentry dependencies:
@@ -96773,7 +97233,7 @@ In `src/modules/wishlist/service.ts`, add the following imports and method:
// other imports...
import { InjectManager } from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
export default class WishlistModuleService extends MedusaService({
Wishlist,
@@ -115723,7 +116183,7 @@ Before adding the step that does this, you'll add a method in the `RestockModule
// other imports...
import { InjectManager, MedusaContext } from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
class RestockModuleService extends MedusaService({
RestockSubscription,

View File

@@ -679,7 +679,7 @@ export const sidebars = [
{
type: "link",
path: "/learn/production/worker-mode",
title: "Worker Mode",
title: "Worker Modes",
},
{
type: "link",
@@ -709,6 +709,18 @@ export const sidebars = [
path: "https://github.com/medusajs/medusa/releases",
title: "Release Notes",
},
{
type: "link",
path: "/learn/codemods",
title: "Codemods",
children: [
{
type: "link",
title: "Replace Imports (v2.11.0+)",
path: "/learn/codemods/replace-imports",
},
],
},
],
},
{

View File

@@ -75,12 +75,12 @@ The [ApplicationMethod data model](/references/promotion/models/ApplicationMetho
</Table.Cell>
<Table.Cell>
Is the discounted amount applied to each item or split between the applicable items?
Is the discounted amount applied to each item, split between the applicable items, or applied on specific number of items?
</Table.Cell>
<Table.Cell>
`each`, `across`
`each`, `across`, `once`
</Table.Cell>
</Table.Row>
@@ -113,7 +113,11 @@ In this example, the cart must have two product variants with the SKU `SHIRT` fo
## Maximum Quantity Restriction
When the `allocation` property in the `ApplicationMethod` is set to `each`, you can set the `max_quantity` property of `ApplicationMethod` to limit how many item quantities the promotion is applied to.
You can restrict how many items the promotion is applied to either at the item level or the cart level.
### Item Level Restriction
When the `allocation` property in the `ApplicationMethod` is set to `each`, you can set the `max_quantity` property of `ApplicationMethod` to limit how many quantities of each applicable item the promotion is applied to.
For example, if the `max_quantity` property is set to `1` and the customer has a line item with quantity two in the cart, the promotion is only applied to one of them.
@@ -147,4 +151,112 @@ This condition is applied on the quantity of every applicable item in the cart.
]
}
}
```
```
### Cart Level Restriction
<Note>
The `once` allocation type is available from [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0).
</Note>
When the `allocation` property in the `ApplicationMethod` is set to `once`, you must set the `max_quantity` property of `ApplicationMethod`. It limits how many items in total the promotion is applied to.
In this scenario, the Promotion Module prioritizes which applicable items the promotion is applied to based on the following rules:
1. Prioritize items with the lowest price.
2. Distribute the promotion sequentially until the `max_quantity` is reached.
#### Example 1
Consider:
- A promotion whose application method has its `allocation` property set to `once` and `max_quantity` set to `2`.
- A cart with three items having different prices, each with a quantity of `1`.
The Promotion Module will apply the promotion to the two items with the lowest price.
```json title="Example Cart"
{
"cart": {
"items": [
{
"id": "item_1",
"price": 10,
"quantity": 1 // The promotion is applied to this item
},
{
"id": "item_2",
"price": 20,
"quantity": 1 // The promotion is applied to this item
},
{
"id": "item_3",
"price": 30,
"quantity": 1 // The promotion is NOT applied to this item
}
]
}
}
```
#### Example 2
Consider:
- A promotion whose application method has its `allocation` property set to `once` and `max_quantity` set to `2`.
- A cart with two items having different prices and quantities greater than `2`.
The Promotion Module will try to apply the promotion to the item with the lowest price first:
```json title="Example Cart"
{
"cart": {
"items": [
{
"id": "item_1",
"price": 10,
"quantity": 3 // The promotion is applied to 2 of this item
},
{
"id": "item_2",
"price": 20,
"quantity": 4 // The promotion is NOT applied to this item
}
]
}
}
```
Since that item has a quantity of `3`, the promotion is applied to `2` of that item, reaching the `max_quantity` limit. The promotion is not applied to the other item.
#### Example 3
Consider:
- A promotion whose application method has its `allocation` property set to `once` and `max_quantity` set to `5`.
- A cart with two items having different prices and quantities less than `5`.
The Promotion Module will try to apply the promotion to the item with the lowest price first:
```json title="Example Cart"
{
"cart": {
"items": [
{
"id": "item_1",
"price": 10,
"quantity": 3 // The promotion is applied to all 3 of this item
},
{
"id": "item_2",
"price": 20,
"quantity": 4 // The promotion is applied to 2 of this item
}
]
}
}
```
The promotion is applied to all `3` quantities of the item with the lowest price. Since the `max_quantity` is `5`, the promotion is applied to `2` quantities of the other item, reaching the `max_quantity` limit.

View File

@@ -14,31 +14,76 @@ Refer to this [Medusa Admin User Guide](!user-guide!/promotions/campaigns) to le
## What is a Campaign?
A [Campaign](/references/promotion/models/Campaign) combines [promotions](../concepts/page.mdx#what-is-a-promotion) under the same conditions, such as start and end dates.
A [Campaign](/references/promotion/models/Campaign) groups [promotions](../concepts/page.mdx#what-is-a-promotion) under the same conditions, such as start and end dates.
Campaigns are useful for grouping promotions that share the same time frame or target audience. They're also useful for limiting the usage of promotions.
Use campaigns to group promotions that share the same time frame or target audience, and to limit promotion usage.
![A diagram showcasing the relation between the Campaign and Promotion data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709899225/Medusa%20Resources/campagin-promotion_hh3qsi.jpg)
---
## Campaign Limits
## Limit Promotion Usage with Campaign Budgets
Each campaign can have a budget represented by the [CampaignBudget data model](/references/promotion/models/CampaignBudget). The budget limits how many times the promotion can be used.
Each campaign can have a budget represented by the [CampaignBudget data model](/references/promotion/models/CampaignBudget). The budget limits how many times a promotion can be used.
There are two types of budgets:
There are three types of budgets: two that are global and one that is based on cart attributes.
- `spend`: An amount that, when crossed, the promotion becomes unusable.
- For example, if the amount limit is set to `$100`, and the total amount of usage of this promotion crosses that threshold, the promotion can no longer be applied.
- `usage`: The number of times that a promotion can be used.
- For example, if the usage limit is set to `10`, the promotion can be used only 10 times by customers. After that, it can no longer be applied.
### Global Budgets
A global budget limits promotion usage without considering any cart attributes.
There are two types of global budgets:
- `spend`: An amount that, when exceeded, makes the promotion unusable.
- For example, if the amount limit is `$100` and the total usage of this promotion exceeds that threshold, the promotion can no longer be applied.
- `usage`: The number of times a promotion can be used.
- For example, if the usage limit is `10`, customers can use the promotion only 10 times. After that, it can no longer be applied.
![A diagram showcasing the relation between the Campaign and CampaignBudget data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709899463/Medusa%20Resources/campagin-budget_rvqlmi.jpg)
### How Budgets Limit Promotion Usage
Global budgets track usage and limits through the following properties of the `CampaignBudget` data model:
When a customer tries to use a promotion, Medusa checks whether the campaign has a budget and if the budget limit has been reached. If so, the promotion cannot be applied.
- `limit`: The maximum amount or number of uses allowed for the promotion.
- `used`: The current amount spent or number of times the promotion has been used.
For example, if a campaign has a budget of type `usage` with a limit of `10`, and the promotion has already been used 10 times, it cannot be applied anymore and is considered expired.
### Attribute-based Budgets
However, once a promotion is applied to a cart, it remains valid until the order is completed, even if the budget limit is reached in the meantime. This ensures that customers who have already applied the promotion can still benefit from it during checkout.
<Note>
Attribute-based budgets were introduced in [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0).
</Note>
An attribute-based budget limits promotion usage based on a cart attribute. Use these budget types to have granular control over how many times a promotion can be used based on specific attributes.
There's one type of attribute-based budget, which is `use_by_attribute`. It allows you to limit the number of times a promotion can be used based on a specific cart attribute.
#### Allowed Attributes
There are two attributes that you can limit promotion usage by:
- `customer_id`: Limits promotion usage based on the unique identifier of a customer.
- `customer_email`: Limits promotion usage based on the email address of a customer.
These attributes are compared against the cart's `customer_id` or `email` to determine how many times the promotion has been used for that specific attribute value, and whether the budget limit has been reached.
#### Tracking Attribute-based Usage
The `CampaignBudgetUsage` data model tracks the usage of attribute-based budgets. It tracks how many times a promotion has been used for each unique attribute value. It includes the following properties:
1. `attribute_value`: The value of the attribute, such as a specific customer ID or email.
2. `used`: The number of times the promotion has been used for that attribute value.
For example, if the attribute is `customer_id`, a new `CampaignBudgetUsage` record is created for each customer that uses the promotion to track their individual usage. Once a customer exceeds the limit set in the `CampaignBudget`, they can no longer use the promotion.
![A diagram showcasing the relation between the CampaignBudget and CampaignBudgetUsage data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1760340527/Medusa%20Resources/campaign-budget-attr_fv0v2u.jpg)
---
## How Campaign Budgets Limit Promotion Usage
When a customer tries to use a promotion, Medusa checks whether the campaign has a budget and if the budget limit has been reached. If the limit is reached, the promotion cannot be applied.
For example, if a campaign has a `usage` budget with a limit of `10` and the promotion has already been used 10 times, it can no longer be applied and is considered expired.
However, once a promotion is applied to a cart, it remains valid until the order is completed, even if the budget limit is reached in the meantime. This ensures that customers who already applied the promotion can still benefit from it during checkout.

View File

@@ -36,6 +36,12 @@ This reference assumes you've already resolved the data model repository, as exp
## Create Records
<Note>
As of [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0), MikroORM dependencies are included in the `@medusajs/framework` package. If you're using an older version of Medusa, change the import statement to `@mikro-orm/knex`.
</Note>
```ts
import {
InjectTransactionManager,
@@ -43,7 +49,7 @@ import {
MedusaService,
} from "@medusajs/framework/utils"
import { Context, InferTypeOf, DAL } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
import Post from "./models/post"
class BlogModuleService extends MedusaService({

View File

@@ -35,6 +35,12 @@ This reference assumes you've already resolved the data model repository, as exp
## Delete Record by ID
<Note>
As of [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0), MikroORM dependencies are included in the `@medusajs/framework` package. If you're using an older version of Medusa, change the import statement to `@mikro-orm/knex`.
</Note>
```ts
import {
InjectTransactionManager,
@@ -42,7 +48,7 @@ import {
MedusaService,
} from "@medusajs/framework/utils"
import { Context, InferTypeOf, DAL } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
import Post from "./models/post"
class BlogModuleService extends MedusaService({
@@ -92,7 +98,7 @@ import {
MedusaService,
} from "@medusajs/framework/utils"
import { Context, InferTypeOf, DAL } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
import Post from "./models/post"
class BlogModuleService extends MedusaService({

View File

@@ -106,7 +106,7 @@ import {
MedusaService,
} from "@medusajs/framework/utils"
import { Context, InferTypeOf, DAL } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
import Post from "./models/post"
class BlogModuleService extends MedusaService({
@@ -149,7 +149,7 @@ import {
MedusaService,
} from "@medusajs/framework/utils"
import { Context, InferTypeOf, DAL } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
import Post from "./models/post"
class BlogModuleService extends MedusaService({
@@ -213,7 +213,7 @@ import {
MedusaService,
} from "@medusajs/framework/utils"
import { Context, InferTypeOf, DAL } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
import Post from "./models/post"
class BlogModuleService extends MedusaService({
@@ -285,7 +285,7 @@ import {
MedusaService,
} from "@medusajs/framework/utils"
import { Context, InferTypeOf, DAL } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
import Post from "./models/post"
class BlogModuleService extends MedusaService({
@@ -349,7 +349,7 @@ import {
MedusaService,
} from "@medusajs/framework/utils"
import { Context, InferTypeOf, DAL } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
import Post from "./models/post"
class BlogModuleService extends MedusaService({
@@ -403,6 +403,12 @@ The method returns an array of records. The number of records is less than or eq
## Sort Records
<Note>
As of [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0), MikroORM dependencies are included in the `@medusajs/framework` package. If you're using an older version of Medusa, change the import statement to `@mikro-orm/knex`.
</Note>
```ts title="src/modules/blog/service.ts"
import {
InjectTransactionManager,
@@ -410,7 +416,7 @@ import {
MedusaService,
} from "@medusajs/framework/utils"
import { Context, InferTypeOf, DAL } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
import Post from "./models/post"
class BlogModuleService extends MedusaService({

View File

@@ -99,6 +99,12 @@ This reference assumes you've already resolved the data model repository, as exp
To retrieve a list of records matching a set of filters, use the `findAndCount` method of the data model repository:
<Note>
As of [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0), MikroORM dependencies are included in the `@medusajs/framework` package. If you're using an older version of Medusa, change the import statement to `@mikro-orm/knex`.
</Note>
```ts title="src/modules/blog/service.ts"
import {
InjectTransactionManager,
@@ -106,7 +112,7 @@ import {
MedusaService,
} from "@medusajs/framework/utils"
import { Context, InferTypeOf, DAL } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
import Post from "./models/post"
class BlogModuleService extends MedusaService({
@@ -156,7 +162,7 @@ import {
MedusaService,
} from "@medusajs/framework/utils"
import { Context, InferTypeOf, DAL } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
import Post from "./models/post"
class BlogModuleService extends MedusaService({
@@ -226,7 +232,7 @@ import {
MedusaService,
} from "@medusajs/framework/utils"
import { Context, InferTypeOf, DAL } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
import Post from "./models/post"
class BlogModuleService extends MedusaService({
@@ -306,7 +312,7 @@ import {
MedusaService,
} from "@medusajs/framework/utils"
import { Context, InferTypeOf, DAL } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
import Post from "./models/post"
class BlogModuleService extends MedusaService({
@@ -378,7 +384,7 @@ import {
MedusaService,
} from "@medusajs/framework/utils"
import { Context, InferTypeOf, DAL } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
import Post from "./models/post"
class BlogModuleService extends MedusaService({
@@ -439,7 +445,7 @@ import {
MedusaService,
} from "@medusajs/framework/utils"
import { Context, InferTypeOf, DAL } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
import Post from "./models/post"
class BlogModuleService extends MedusaService({

View File

@@ -37,6 +37,12 @@ This reference assumes you've already resolved the data model repository, as exp
## Restore Record by ID
<Note>
As of [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0), MikroORM dependencies are included in the `@medusajs/framework` package. If you're using an older version of Medusa, change the import statement to `@mikro-orm/knex`.
</Note>
```ts
import {
InjectTransactionManager,
@@ -44,7 +50,7 @@ import {
MedusaService,
} from "@medusajs/framework/utils"
import { Context, InferTypeOf, DAL } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
import Post from "./models/post"
class BlogModuleService extends MedusaService({
@@ -140,7 +146,7 @@ import {
MedusaService,
} from "@medusajs/framework/utils"
import { Context, InferTypeOf, DAL } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
import Post from "./models/post"
class BlogModuleService extends MedusaService({
@@ -236,7 +242,7 @@ import {
MedusaService,
} from "@medusajs/framework/utils"
import { Context, InferTypeOf, DAL } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
import Post from "./models/post"
class BlogModuleService extends MedusaService({

View File

@@ -39,6 +39,12 @@ This reference assumes you've already resolved the data model repository, as exp
## Soft-Delete Record by ID
<Note>
As of [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0), MikroORM dependencies are included in the `@medusajs/framework` package. If you're using an older version of Medusa, change the import statement to `@mikro-orm/knex`.
</Note>
```ts
import {
InjectTransactionManager,
@@ -46,7 +52,7 @@ import {
MedusaService,
} from "@medusajs/framework/utils"
import { Context, InferTypeOf, DAL } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
import Post from "./models/post"
class BlogModuleService extends MedusaService({
@@ -142,7 +148,7 @@ import {
MedusaService,
} from "@medusajs/framework/utils"
import { Context, InferTypeOf, DAL } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
import Post from "./models/post"
class BlogModuleService extends MedusaService({
@@ -238,7 +244,7 @@ import {
MedusaService,
} from "@medusajs/framework/utils"
import { Context, InferTypeOf, DAL } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
import Post from "./models/post"
class BlogModuleService extends MedusaService({

View File

@@ -50,6 +50,12 @@ This reference assumes you've already resolved the data model repository, as exp
## Update Records
<Note>
As of [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0), MikroORM dependencies are included in the `@medusajs/framework` package. If you're using an older version of Medusa, change the import statement to `@mikro-orm/knex`.
</Note>
```ts
import {
InjectTransactionManager,
@@ -57,7 +63,7 @@ import {
MedusaService,
} from "@medusajs/framework/utils"
import { Context, InferTypeOf, DAL } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
import Post from "./models/post"
class BlogModuleService extends MedusaService({

View File

@@ -35,6 +35,12 @@ This reference assumes you've already resolved the data model repository, as exp
## Upsert Records
<Note>
As of [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0), MikroORM dependencies are included in the `@medusajs/framework` package. If you're using an older version of Medusa, change the import statement to `@mikro-orm/knex`.
</Note>
```ts
import {
InjectTransactionManager,
@@ -42,7 +48,7 @@ import {
MedusaService,
} from "@medusajs/framework/utils"
import { Context, InferTypeOf, DAL } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
import Post from "./models/post"
class BlogModuleService extends MedusaService({

View File

@@ -41,6 +41,12 @@ This reference assumes you've already resolved the data model repository, as exp
## Upsert Records
<Note>
As of [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0), MikroORM dependencies are included in the `@medusajs/framework` package. If you're using an older version of Medusa, change the import statement to `@mikro-orm/knex`.
</Note>
```ts
import {
InjectTransactionManager,
@@ -48,7 +54,7 @@ import {
MedusaService,
} from "@medusajs/framework/utils"
import { Context, InferTypeOf, DAL } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
import Post from "./models/post"
class BlogModuleService extends MedusaService({

View File

@@ -1646,7 +1646,7 @@ import {
MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
class BlogModuleService {
// ...
@@ -2971,7 +2971,7 @@ To register a resource in the Module's container using a loader, use the `contai
import {
LoaderOptions,
} from "@medusajs/framework/types"
import { asValue } from "awilix"
import { asValue } from "@medusajs/framework/awilix"
export default async function helloWorldLoader({
container,

View File

@@ -1484,7 +1484,7 @@ In `src/modules/product-review/service.ts`, add the following methods to the `Pr
import { InjectManager, MedusaService, MedusaContext } from "@medusajs/framework/utils"
import Review from "./models/review"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
class ProductReviewModuleService extends MedusaService({
Review,

View File

@@ -175,7 +175,7 @@ export const loaderHighlights = [
```ts title="src/modules/contentful/loader/create-content-models.ts" highlights={loaderHighlights}
import { LoaderOptions } from "@medusajs/framework/types"
import { asValue } from "awilix"
import { asValue } from "@medusajs/framework/awilix"
import { createClient } from "contentful-management"
import { MedusaError } from "@medusajs/framework/utils"

View File

@@ -94,15 +94,7 @@ Refer to the [Instrumentation](!docs!/learn/debugging-and-testing/instrumentatio
### a. Install Instrumentation Dependencies
To set up instrumentation in Medusa, you need to install the necessary OpenTelemetry dependencies.
In your Medusa application's directory, run the following command:
```bash npm2yarn
npm install @opentelemetry/sdk-node @opentelemetry/resources @opentelemetry/sdk-trace-node @opentelemetry/instrumentation-pg
```
Then, you need to install the dependencies necessary for the monitoring tool you want to use, which is Sentry in this case.
To set up instrumentation in Medusa, you need to install the dependencies necessary for the monitoring tool you want to use, which is Sentry in this case.
So, run the following command to install the necessary Sentry dependencies:

View File

@@ -65,19 +65,66 @@ npx medusa start
<Table.Row>
<Table.Cell>
`--cluster <number>`
`--cluster <string> [--workers <string>] [--servers <string>]`
</Table.Cell>
<Table.Cell>
Start Medusa's Node.js server in [cluster mode](https://expressjs.com/en/advanced/best-practice-performance.html#run-your-app-in-a-cluster). Running in cluster mode significantly improves performance as the workload and tasks are distributed among all available instances instead of a single one.
Start Medusa in cluster mode. Learn more in the [Starting Medusa in Cluster Mode](#starting-medusa-in-cluster-mode) section.
</Table.Cell>
<Table.Cell>
Cluster mode is disabled by default. If the option is passed but no number is passed, Medusa will try to consume all available CPU cores.
Cluster mode is disabled by default. If the option is passed but no number or percentage is passed, Medusa will try to consume all available CPU cores.
</Table.Cell>
</Table.Row>
</Table.Body>
</Table>
</Table>
---
## Starting Medusa in Cluster Mode
<Note>
Prior to [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0), the `--cluster` option accepted a number value only. You can now pass either a number or a percentage value, and you can also specify the number of servers and workers.
</Note>
Medusa supports starting the Node.js server in [cluster mode](https://expressjs.com/en/advanced/best-practice-performance.html#run-your-app-in-a-cluster), which significantly improves performance as the workload and tasks are distributed among all available instances instead of a single one.
Cluster mode is disabled by default. To enable it, pass the `--cluster` option when starting Medusa:
```bash
npx medusa start --cluster
```
When the `--cluster` option is passed without a number or percentage value, Medusa will try to consume all available CPU cores.
### Specify Number or Percentage of CPU Cores
You can specify the number or percentage of CPU cores to be used by passing a number or percentage value to the `--cluster` option:
```bash
npx medusa start --cluster 2 # Use 2 CPU cores
npx medusa start --cluster 50% # Use 50% of available CPU
```
### Specify Number of Servers and Workers
When running Medusa in cluster mode, you can specify the number or percentage of instances that are [servers or workers](!docs!/learn/production/worker-mode) by passing the `--servers` and `--workers` options:
```bash
npx medusa start --cluster 4 --servers 25% --workers 75% # Use 4 CPU cores, with 25% as servers and 75% as workers
npx medusa start --cluster 4 --servers 1 --workers 3 # Use 4 CPU cores, with 1 as server and 3 as workers
npx medusa start --cluster 4 --servers 1 --workers 1 # Use 4 CPU cores, with 1 as server and 1 as worker (the remaining 2 will run in shared mode)
```
When the number or percentage of servers and workers don't add up to the total number of instances in cluster mode, the remaining instances will run in shared mode.
<Note>
Learn more in the [Worker Mode](!docs!/learn/production/worker-mode) guide.
</Note>

View File

@@ -1958,7 +1958,7 @@ In `src/modules/wishlist/service.ts`, add the following imports and method:
// other imports...
import { InjectManager } from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
export default class WishlistModuleService extends MedusaService({
Wishlist,

View File

@@ -1133,7 +1133,7 @@ Before adding the step that does this, you'll add a method in the `RestockModule
// other imports...
import { InjectManager, MedusaContext } from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { EntityManager } from "@medusajs/framework/@mikro-orm/knex"
class RestockModuleService extends MedusaService({
RestockSubscription,

View File

@@ -61,7 +61,9 @@ In this example, you retrieve the product variants' inventory quantity by passin
A variant is in stock if:
1. Its `manage_inventory`'s value is `false`, meaning that Medusa doesn't keep track of its inventory.
2. If its `inventory_quantity`'s value is greater than `0`. This property is only available on variants whose `manage_inventory` is `false`.
2. If its `inventory_quantity`'s value is greater than `0`.
- This property is only available on variants whose `manage_inventory` is `false`.
- If the variant doesn't have inventory levels in the stock location associated with the API's scope, its `inventory_quantity` will be `null`.
---

View File

@@ -24,7 +24,7 @@ module.exports = {
Then, create the `integration-tests/setup.js` file with the following content:
```js title="integration-tests/setup.js"
const { MetadataStorage } = require("@mikro-orm/core")
const { MetadataStorage } = require("@medusajs/framework/@mikro-orm/core")
MetadataStorage.clear()
```

View File

@@ -68,8 +68,8 @@ export const generatedEditDates = {
"app/commerce-modules/promotion/_events/_events-table/page.mdx": "2024-07-03T19:27:13+03:00",
"app/commerce-modules/promotion/_events/page.mdx": "2024-07-03T19:27:13+03:00",
"app/commerce-modules/promotion/actions/page.mdx": "2025-06-27T15:42:19.142Z",
"app/commerce-modules/promotion/application-method/page.mdx": "2025-10-01T09:54:44.931Z",
"app/commerce-modules/promotion/campaign/page.mdx": "2025-02-26T11:32:24.484Z",
"app/commerce-modules/promotion/application-method/page.mdx": "2025-10-14T12:09:22.188Z",
"app/commerce-modules/promotion/campaign/page.mdx": "2025-10-13T07:34:59.008Z",
"app/commerce-modules/promotion/concepts/page.mdx": "2025-02-26T11:31:54.391Z",
"app/commerce-modules/promotion/page.mdx": "2025-04-17T08:48:14.643Z",
"app/commerce-modules/region/_events/_events-table/page.mdx": "2024-07-03T19:27:13+03:00",
@@ -565,7 +565,7 @@ export const generatedEditDates = {
"app/medusa-cli/commands/develop/page.mdx": "2025-09-01T15:31:35.072Z",
"app/medusa-cli/commands/exec/page.mdx": "2025-01-16T09:51:17.050Z",
"app/medusa-cli/commands/new/page.mdx": "2024-08-28T10:43:34.110Z",
"app/medusa-cli/commands/start/page.mdx": "2025-04-08T11:56:15.522Z",
"app/medusa-cli/commands/start/page.mdx": "2025-10-13T10:27:25.345Z",
"app/medusa-cli/commands/telemtry/page.mdx": "2025-01-16T09:51:24.323Z",
"app/medusa-cli/commands/user/page.mdx": "2025-09-01T15:36:38.978Z",
"app/recipes/marketplace/examples/restaurant-delivery/page.mdx": "2025-08-25T07:30:37.299Z",
@@ -877,7 +877,7 @@ export const generatedEditDates = {
"references/promotion/interfaces/promotion.IPromotionModuleService/page.mdx": "2024-11-25T17:49:58.612Z",
"references/types/EventBusTypes/interfaces/types.EventBusTypes.IEventBusService/page.mdx": "2025-04-11T09:04:46.063Z",
"references/types/TransactionBaseTypes/interfaces/types.TransactionBaseTypes.ITransactionBaseService/page.mdx": "2024-09-06T00:11:08.494Z",
"app/storefront-development/products/inventory/page.mdx": "2025-03-27T14:46:51.435Z",
"app/storefront-development/products/inventory/page.mdx": "2025-10-13T10:18:19.391Z",
"references/auth/IAuthModuleService/methods/auth.IAuthModuleService.updateAuthIdentities/page.mdx": "2025-05-20T07:51:40.735Z",
"references/auth/IAuthModuleService/methods/auth.IAuthModuleService.updateProvider/page.mdx": "2025-05-20T07:51:40.735Z",
"references/auth/IAuthModuleService/methods/auth.IAuthModuleService.updateProviderIdentities/page.mdx": "2025-05-20T07:51:40.735Z",
@@ -2165,7 +2165,7 @@ export const generatedEditDates = {
"app/commerce-modules/sales-channel/links-to-other-modules/page.mdx": "2025-04-17T16:00:09.483Z",
"app/commerce-modules/stock-location/links-to-other-modules/page.mdx": "2025-04-17T16:02:51.467Z",
"app/commerce-modules/store/links-to-other-modules/page.mdx": "2025-04-17T16:03:16.419Z",
"app/examples/page.mdx": "2025-07-16T09:53:26.163Z",
"app/examples/page.mdx": "2025-09-29T15:36:18.867Z",
"app/medusa-cli/commands/build/page.mdx": "2025-09-01T15:30:05.995Z",
"app/js-sdk/page.mdx": "2025-08-01T14:17:07.509Z",
"references/js_sdk/admin/Admin/properties/js_sdk.admin.Admin.apiKey/page.mdx": "2025-05-20T07:51:40.924Z",
@@ -5564,7 +5564,7 @@ export const generatedEditDates = {
"references/modules/sales_channel_models/page.mdx": "2024-12-10T14:55:13.205Z",
"references/types/DmlTypes/types/types.DmlTypes.KnownDataTypes/page.mdx": "2024-12-17T16:57:19.922Z",
"references/types/DmlTypes/types/types.DmlTypes.RelationshipTypes/page.mdx": "2024-12-10T14:54:55.435Z",
"app/recipes/commerce-automation/restock-notification/page.mdx": "2025-07-14T09:35:35.226Z",
"app/recipes/commerce-automation/restock-notification/page.mdx": "2025-09-29T15:35:49.320Z",
"app/integrations/guides/shipstation/page.mdx": "2025-05-20T07:51:40.717Z",
"app/nextjs-starter/guides/customize-stripe/page.mdx": "2025-07-15T08:50:51.997Z",
"references/core_flows/Cart/Workflows_Cart/functions/core_flows.Cart.Workflows_Cart.listShippingOptionsForCartWithPricingWorkflow/page.mdx": "2025-09-18T17:04:38.644Z",
@@ -5831,7 +5831,7 @@ export const generatedEditDates = {
"references/core_flows/types/core_flows.ThrowUnlessPaymentCollectionNotePaidInput/page.mdx": "2025-06-25T10:11:33.516Z",
"references/core_flows/types/core_flows.ValidatePaymentsRefundStepInput/page.mdx": "2025-06-25T10:11:34.185Z",
"references/core_flows/types/core_flows.ValidateRefundStepInput/page.mdx": "2025-09-12T14:10:36.022Z",
"app/plugins/guides/wishlist/page.mdx": "2025-08-15T10:52:37.465Z",
"app/plugins/guides/wishlist/page.mdx": "2025-09-29T15:35:46.093Z",
"app/plugins/page.mdx": "2025-02-26T11:39:25.709Z",
"app/admin-components/components/data-table/page.mdx": "2025-03-03T14:55:58.556Z",
"references/order_models/variables/order_models.Order/page.mdx": "2025-09-18T17:05:02.047Z",
@@ -5870,7 +5870,7 @@ export const generatedEditDates = {
"references/types/interfaces/types.BaseProductTypeListParams/page.mdx": "2025-01-27T11:43:54.550Z",
"references/core_flows/Order/Steps_Order/variables/core_flows.Order.Steps_Order.updateOrderChangesStepId/page.mdx": "2025-01-27T11:43:49.278Z",
"app/commerce-modules/payment/account-holder/page.mdx": "2025-04-07T07:31:20.235Z",
"app/troubleshooting/test-errors/page.mdx": "2025-01-31T13:08:42.639Z",
"app/troubleshooting/test-errors/page.mdx": "2025-09-29T15:35:52.805Z",
"app/commerce-modules/product/variant-inventory/page.mdx": "2025-04-25T13:25:02.408Z",
"app/examples/guides/custom-item-price/page.mdx": "2025-06-26T11:53:06.748Z",
"references/core_flows/Cart/Steps_Cart/functions/core_flows.Cart.Steps_Cart.validateShippingStep/page.mdx": "2025-04-11T09:04:35.729Z",
@@ -6542,7 +6542,7 @@ export const generatedEditDates = {
"references/utils/types/utils.NormalizedRow/page.mdx": "2025-06-05T19:05:53.365Z",
"references/utils/utils.Payment/page.mdx": "2025-06-05T19:05:53.489Z",
"app/integrations/guides/slack/page.mdx": "2025-06-26T12:57:20.880Z",
"app/integrations/guides/sentry/page.mdx": "2025-06-16T10:11:29.955Z",
"app/integrations/guides/sentry/page.mdx": "2025-10-02T10:30:51.194Z",
"app/integrations/guides/mailchimp/page.mdx": "2025-06-26T11:59:15.303Z",
"app/how-to-tutorials/tutorials/first-purchase-discounts/page.mdx": "2025-06-26T11:55:27.175Z",
"references/types/CommonTypes/interfaces/types.CommonTypes.CookieOptions/page.mdx": "2025-06-25T10:11:37.088Z",
@@ -6611,6 +6611,15 @@ export const generatedEditDates = {
"app/integrations/guides/meilisearch/page.mdx": "2025-10-09T11:30:25.084Z",
"app/nextjs-starter/guides/storefront-returns/page.mdx": "2025-09-22T06:02:00.580Z",
"references/js_sdk/admin/Admin/properties/js_sdk.admin.Admin.views/page.mdx": "2025-09-18T17:04:59.240Z",
"app/data-model-repository-reference/methods/create/page.mdx": "2025-10-09T11:42:23.826Z",
"app/data-model-repository-reference/methods/delete/page.mdx": "2025-10-09T11:42:59.141Z",
"app/data-model-repository-reference/methods/find/page.mdx": "2025-10-09T11:43:05.478Z",
"app/data-model-repository-reference/methods/findAndCount/page.mdx": "2025-10-09T11:43:49.941Z",
"app/data-model-repository-reference/methods/restore/page.mdx": "2025-10-09T11:44:02.906Z",
"app/data-model-repository-reference/methods/softDelete/page.mdx": "2025-10-09T11:44:16.158Z",
"app/data-model-repository-reference/methods/upsert/page.mdx": "2025-10-09T11:44:39.838Z",
"app/data-model-repository-reference/methods/update/page.mdx": "2025-10-09T11:44:27.403Z",
"app/data-model-repository-reference/methods/upsertWithReplace/page.mdx": "2025-10-09T11:44:53.535Z",
"app/how-to-tutorials/tutorials/agentic-commerce/page.mdx": "2025-10-09T11:25:48.831Z",
"app/storefront-development/production-optimizations/page.mdx": "2025-10-03T13:28:37.909Z",
"app/troubleshooting/subscribers/not-working/page.mdx": "2025-10-16T09:25:57.376Z"

View File

@@ -6419,6 +6419,14 @@ const generatedgeneratedCommerceModulesSidebarSidebar = {
"path": "https://docs.medusajs.com/user-guide/orders/returns",
"children": []
},
{
"loaded": true,
"isPathHref": true,
"type": "ref",
"title": "Manage Refund Reasons",
"path": "https://docs.medusajs.com/user-guide/settings/refund-reasons",
"children": []
},
{
"loaded": true,
"isPathHref": true,

View File

@@ -93,7 +93,13 @@ To capture an orders payment:
## Refund Payment
If you've made changes to an order, such as return or exchange items, that resulted in a negative outstanding amount, you can refund a previously-captured payment.
You can refund a payment either partially or in full. This can be done for various reasons, such as when a customer returns an item or if there was an error in the order.
<Note>
Prior to [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0), you were only able to refund a payment after creating a return or making an order change. From Medusa v2.11.0 onwards, you can refund a payment directly from the order details page without needing to create a return or make an order change. If you're unable to refund a payment, contact your technical team to [update your Medusa application](!docs!/learn/update).
</Note>
Refunding the payment triggers its processing with the chosen payment provider, such as Stripe.
@@ -103,21 +109,21 @@ Refunding payments is irreversible.
</Note>
To refund an orders negative outstanding amount:
To refund an order's captured payment:
1. Open the order's details page.
2. Scroll to the Payment section.
3. Click on the <InlineIcon Icon={EllipsisHorizontal} alt="three-dots" /> icon at the end of the payment to refund.
4. Choose "Refund" from the dropdown.
5. In the side window that opens, fill out the following fields:
- **Select payment to refund**: Choose the payment to refund an amount from.
- **Amount**: Enter the amount to refund.
- **Note**: A note that the customer can see in the notification they receive.
- **Refund Reason**: Select a reason for the refund from the dropdown. You can [manage refund reasons in the settings](../../settings/refund-reasons/page.mdx).
- **Note**: Enter a note that the customer can see in the notification they receive.
6. Once youre done, click on the Save button.
Once the payment is refunded, the customer will receive a notification about the refund and you can view the refund as part of the order's activity.
![Refund Payment Form](https://res.cloudinary.com/dza7lstvk/image/upload/v1739550855/User%20Guide/Screenshot_2025-02-14_at_6.34.01_PM_l9evmh.png)
![Refund Payment Form](https://res.cloudinary.com/dza7lstvk/image/upload/v1759995110/User%20Guide/CleanShot_2025-10-09_at_10.31.35_2x_dvbdbp.png)
---

View File

@@ -131,6 +131,6 @@ To cancel a requested return:
## Refund Customer for a Return
If a return causes an outstanding amount, you can see it in the Summary section of an order. To learn how to refund the payment, refer to [this guide](../payments/page.mdx#refund-payment).
If a return causes an outstanding amount, you can see it in the Summary section of an order. Refer to the [Manage Payments guide](../payments/page.mdx#refund-payment) to learn how to refund the customer for the return.
![Outstanding amount in summary](https://res.cloudinary.com/dza7lstvk/image/upload/v1739799808/User%20Guide/Screenshot_2025-02-17_at_3.42.49_PM_cjxgpg.png)

View File

@@ -56,12 +56,22 @@ To create a campaign:
- **Spend**: The total amount that can be discounted by the promotions in the campaign. Once the total amount discounted exceeds the limit, the campaign and its promotions expire. This is not applicable to "Buy X Get Y" or "Free Shipping" promotions.
- If you chose the **Spend** type, select the currency of the limit in the "Currency" field.
- When adding promotions to the campaign later, promotions that discount a fixed amount can only be added if the currency matches the campaign budget's currency.
- In the Limit field, you can set a campaign budget limit based on the type you chose.
- In the **Limit** field, you can set a campaign budget limit based on the type you chose.
- If you chose **Usage**, enter the number of times the promotions in the campaign can be used.
- If you chose **Spend**, enter the total amount that can be discounted by the promotions in the campaign.
- If you chose the **Usage** type, you can optionally set the **Limit usage per** field to apply the limitation for each customer or email.
- If you don't select any condition, the limit is applied globally. For example, if the limit is set to 5, the promotions in the campaign can be used a total of 5 times.
- If you select "Customer", the limit is applied for each customer based on their unique identifier. For example, if the limit is set to 5, each customer can use the promotions in the campaign 5 times.
- If you select "Customer Email", the limit is applied for each customer based on their email address. For example, if the limit is set to 5, each unique email address can use the promotions in the campaign 5 times.
4. Once you're done, click the Create button.
![Create Campaign Form](https://res.cloudinary.com/dza7lstvk/image/upload/v1739953632/User%20Guide/Screenshot_2025-02-19_at_10.27.00_AM_u4w38d.png)
<Note>
The **Limit usage per** field is available from [Medusa Admin v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0). If you don't see it, request your technical team to [upgrade your Medusa application](!docs!/learn/update).
</Note>
![Create Campaign Form](https://res.cloudinary.com/dza7lstvk/image/upload/v1760349967/User%20Guide/CleanShot_2025-10-13_at_13.05.33_2x_xq5ec5.png)
---
@@ -133,6 +143,23 @@ To edit a campaign's budget usage or spending limit:
---
## Information that Can't be Edited in Campaign
After creating a campaign, you can't edit the following information:
1. The campaign's budget type (usage or spend).
2. The campaign's budget currency (if the type is spend).
3. The campaign's budget "limit usage per" condition (customer or customer email).
If you need to change any of this information, you should either:
1. [Delete the campaign](#delete-campaign).
2. Expire the campaign by [setting an end date in the past](#edit-campaign-details).
Then, create a new campaign with the correct information and [add promotions to it](#manage-promotions-of-campaign).
---
## Manage Promotions of Campaign
From the campaign's details page, you can manage which promotions belong to the campaign.

View File

@@ -77,7 +77,10 @@ If you chose the "Amount off Products" promotion type in the first section, fill
7. In the Maximum Quantity field, set the maximum quantity line-item quantity that the promotion is applied to in the cart.
- For example, if set to `1` and the customer has a line item with quantity two in the cart, the promotion is only applied to one of them.
- This condition is applied on the quantity of every applicable item in the cart. For example, if set to `1` and the customer has two applicable items in the cart, the promotion is applied to one of each of them.
8. In the "What items will the promotion be applied to?" section, you'll specify the items in the cart that the promotion can be applied to. To add a condition, click on the "Add condition" button. For each condition:
8. In the Allocation field, choose how the discount is applied when there are multiple applicable items in the cart:
- **Each**: The discount is applied to each applicable item in the cart.
- **Once**: The discount is applied to a specific number of applicable items in the cart, which is determined by the Maximum Quantity field.
9. In the "What items will the promotion be applied to?" section, you'll specify the items in the cart that the promotion can be applied to. To add a condition, click on the "Add condition" button. For each condition:
- Select the attribute in the first field. This is the attribute that the condition applies to. It can be:
- **Product**: The promotion applies / doesn't apply to the specified product(s).
- **Product Category**: The promotion applies / doesn't apply to the products belonging to the specified categories.
@@ -92,7 +95,7 @@ If you chose the "Amount off Products" promotion type in the first section, fill
- Set the Attribute to Product Category.
- Set the Operator to In.
- Set the Value to the Shirts product category.
9. Once you're done, click Next and move on to the [next step](#step-3-campaign).
10. Once you're done, click Next and move on to the [next step](#step-3-campaign).
![What items will the promotion be applied to section](https://res.cloudinary.com/dza7lstvk/image/upload/v1739898160/User%20Guide/Screenshot_2025-02-18_at_7.02.26_PM_izi1sv.png)
@@ -130,6 +133,12 @@ If you chose the "Amount off Order" promotion type in the first section, fill ou
### c. Percentage off Product
<Note>
If any of the fields are not visible in your admin, contact your technical team to [update your Medusa version](!docs!/learn/update).
</Note>
If you chose the "Percentage off Product" promotion type in the first section, fill out the following in the Details step:
1. For the Method, choose how you want the promotion to be applied:
@@ -158,7 +167,10 @@ If you chose the "Percentage off Product" promotion type in the first section, f
6. In the Promotion Value field, set the amount to be discounted from applicable products when the promotion is applied. The amount is in the currency you chose in the previous section.
7. In the Maximum Quantity field, set the maximum quantity of the applicable that the promotion is applied to in the cart. For example, if set to `1` and the customer has two applicable items in the cart, the promotion is only applied to one of them.
8. In the "What items will the promotion be applied to?" section, you'll specify the items in the cart that the promotion can be applied to. To add a condition, click on the "Add condition" button. For each condition:
8. In the Allocation field, choose how the discount is applied when there are multiple applicable items in the cart:
- **Each**: The discount is applied to each applicable item in the cart.
- **Once**: The discount is applied to a specific number of applicable items in the cart, which is determined by the Maximum Quantity field.
9. In the "What items will the promotion be applied to?" section, you'll specify the items in the cart that the promotion can be applied to. To add a condition, click on the "Add condition" button. For each condition:
- Select the attribute in the first field. This is the attribute that the condition applies to. It can be:
- **Product**: The promotion applies / doesn't apply to the specified product(s).
- **Product Category**: The promotion applies / doesn't apply to the products belonging to the specified categories.
@@ -173,7 +185,7 @@ If you chose the "Percentage off Product" promotion type in the first section, f
- Set the Attribute to Product Category.
- Set the Operator to In.
- Set the Value to the Shirts product category.
9. Once you're done, click Next and move on to the [next step](#step-3-campaign).
10. Once you're done, click Next and move on to the [next step](#step-3-campaign).
![What items will the promotion be applied to section](https://res.cloudinary.com/dza7lstvk/image/upload/v1739898386/User%20Guide/Screenshot_2025-02-18_at_7.06.14_PM_bdi5jv.png)
@@ -332,8 +344,8 @@ In the campaigns section, you can select whether to:
If you selected a new campaign, you need to fill out the following to create a campaign and add the promotion to it:
1. In the Name field, enter the campaign's name.
2. In the Identifier field, enter a custom identifier for the campaign. This is useful if you're associating the campaign with a campaign in an external system, for example.
1. In the **Name** field, enter the campaign's name.
2. In the **Identifier** field, enter a custom identifier for the campaign. This is useful if you're associating the campaign with a campaign in an external system, for example.
3. Optionally enter a description, and start and end dates.
- If a start date is specified, all promotions in the campaign can be used after the specified date.
- If an end date is specified, all promotions in the campaign can't be used after the specified date.
@@ -341,7 +353,17 @@ If you selected a new campaign, you need to fill out the following to create a c
- For Type, you can choose to set the budget either based on:
- **Usage**: How many times the promotions in the campaign can be used. Once the number of times exceed the limit, the campaign and its promotions expire.
- **Spend**: The total amount that can be discounted by the promotions in the campaign. Once the total amount discounted exceeds the limit, the campaign and its promotions expire. This is disabled if you select the "Buy X Get Y" or "Free Shipping" promotion types.
- In the Limit field, enter the budget limit based on the chosen type.
- In the **Limit** field, enter the budget limit based on the chosen type.
- If you chose the **Usage** type, you can optionally set the **Limit usage per** field to apply the limitation for each customer or email.
- If you don't select any condition, the limit is applied globally. For example, if the limit is set to 5, the promotions in the campaign can be used a total of 5 times.
- If you select "Customer", the limit is applied for each customer based on their unique identifier. For example, if the limit is set to 5, each customer can use the promotions in the campaign 5 times.
- If you select "Customer Email", the limit is applied for each customer based on their email address. For example, if the limit is set to 5, each unique email address can use the promotions in the campaign 5 times.
5. Once you're done, click on the Save button.
<Note>
The **Limit usage per** field is available from [Medusa Admin v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0). If you don't see it, request your technical team to [upgrade your Medusa application](!docs!/learn/update).
</Note>
You can now [manage the promotion](../manage/page.mdx) you created.

View File

@@ -69,6 +69,12 @@ A promotion's status is shown in the Promotions listing page, and in the promoti
## Edit Promotion's Details
<Note>
If any of the fields are not visible in your admin, contact your technical team to [update your Medusa version](!docs!/learn/update).
</Note>
To edit a promotion's general details:
1. Go to the promotion's details page.
@@ -78,8 +84,12 @@ To edit a promotion's general details:
- You can change the promotion's status to either **Draft**, **Active**, or **Inactive**. Inactive is useful if the promotion was originally active but you want to expire it.
- You can edit the promotion's method (whether it's applied manually or automatically) and code.
- Based on the promotion's type, you can change how the amount is discounted, either by a fixed amount or a percentage. You can also change the discounted amount.
- Based on the promotion's type, you can change how the promotion is applied, either on every applicable item or across all items in the cart.
- For example, if your promotion discounts an amount from applicable products in a cart, choosing **Each** applies the discount on each applicable product, whereas **Across** applies the discount across all applicable products.
- Based on the promotion's type, you can change how the promotion is applied:
- **Each**: The discount is applied to each applicable item in the cart.
- **Across**: The discount is applied across all applicable items in the cart.
- **Once**: The discount is applied to a specific number of applicable items in the cart, which is determined by the Maximum Quantity field.
- Based on the promotion's type, some of the above options may not be available. For example, for "Amount off order", only "Across" is available.
- You can edit the maximum quantity of applicable items that the promotion is applied to in the cart.
5. Once you're done, click the Save button.
![Edit Promotion Form](https://res.cloudinary.com/dza7lstvk/image/upload/v1739951499/User%20Guide/Screenshot_2025-02-19_at_9.51.26_AM_zylmig.png)

View File

@@ -44,7 +44,13 @@ Here, you can see a list of all the secret API keys for the logged-in user. You
## Create Secret API Key
When you create a secret API key, you create it for the currently logged-in user. A user can have one active secret key at a time. So, if you already have one, you must [revoke it](#revoke-secret-api-key) before creating a new one.
When you create a secret API key, you create it for the currently logged-in user.
<Note>
Prior to [Medusa v2.11.0], users could only have one active secret API key at a time. To create a new secret API key, you had to [revoke](#revoke-secret-api-key) the existing key first. If you can't create multiple secret API keys, request your technical team to [upgrade your Medusa application](!docs!/learn/update).
</Note>
To create a new secret API key for the currently logged-in user:

View File

@@ -52,7 +52,7 @@ To create a stock location:
To view the details of a location:
1. Go to Settings → Locations.
2. Click on "View details" in the header of the location's section.
2. Click on the location you want to view.
This opens the location's details page where you can also manage the location.

View File

@@ -19,19 +19,31 @@ export const metadata = {
In the Locations & Shipping settings, you can manage stock locations and their shipping options, and other shipping-related configurations, such as shipping profiles.
## Overview
### Stock Locations
A stock location:
- Is associated with sales channels. Orders from these sales channels are fulfilled from this location.
- Has fulfillment providers that can be used in that location. For example, you can use UPS in the United States and DHL in Europe.
- Has shipping and pick up modes that you can add shipping options to. These are the shipping options used to fulfill items from this stock location.
### Shipping Profiles
A shipping profile groups similar products that require a different way of fulfillment. For example, fragile products are fulfilled differently than normal products. You can then provide shipping options specific for products that belong to the Fragile shipping profile.
### Shipping Option Types
A shipping option type groups shipping options with similar characteristics. For example, you can group all express shipping options together and apply a promotion to all of them at once.
---
## View Location & Shipping Settings
To view location and shipping settings, go to Settings → Locations & Shipping.
![Location & shipping settings](https://res.cloudinary.com/dza7lstvk/image/upload/v1756389687/User%20Guide/CleanShot_2025-08-28_at_17.00.53_2x_kymfeh.png)
![Location & shipping settings](https://res.cloudinary.com/dza7lstvk/image/upload/v1759995534/User%20Guide/CleanShot_2025-10-09_at_10.38.39_2x_eqsrxl.png)
---

View File

@@ -27,6 +27,7 @@ You can also go back to the main sidebar by clicking the <InlineIcon Icon={Arrow
- [Manage Regions](./regions/page.mdx)
- [Manage Tax Regions](./tax-regions/page.mdx)
- [Manage Return Reasons](./return-reasons/page.mdx)
- [Manage Refund Reasons](./refund-reasons/page.mdx)
- [Manage Sales Channels](./sales-channels/page.mdx)
- [Manage Product Types](./product-types/page.mdx)
- [Manage Product Tags](./product-tags/page.mdx)

View File

@@ -0,0 +1,88 @@
---
sidebar_position: 5
sidebar_label: "Manage Refund Reasons"
tags:
- user guide
- order
products:
- order
---
import { EllipsisHorizontal } from "@medusajs/icons"
export const metadata = {
title: `Manage Refund Reasons in Medusa Admin`,
}
# {metadata.title}
In this guide, youll learn what refund reasons are and how to manage them.
<Note>
Refund reason management is available from [Medusa Admin v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0). If you don't see the Refund Reasons option in your Medusa Admin, request your technical team to [upgrade your Medusa application](!docs!/learn/update).
</Note>
## What's a Refund Reason?
A refund reason is a predefined reason for refunding an order. It helps you categorize and track refunds in your store.
When [processing a refund](../../orders/payments/page.mdx#refund-payment), you can select a reason from the list of refund reasons you have set up.
---
## View Refund Reasons
To view refund reasons in your store, go to Settings → Refund Reasons. Here, you can see a list of all the refund reasons you have set up in your store. You can also search, filter, and sort the refund reasons to find the one you are looking for.
Medusa adds some default refund reasons to your store. You can use or [delete](#delete-a-refund-reason) them.
![Refund reasons list](https://res.cloudinary.com/dza7lstvk/image/upload/v1759994483/User%20Guide/CleanShot_2025-10-09_at_10.20.53_2x_sowoag.png)
---
## Create Refund Reason
To create a refund reason:
1. Go to Settings → Refund Reasons.
2. Click the Create button in the main section's header.
3. In the form that opens:
- **Label**: Enter what is shown to customers or admin users.
- **Code**: Enter a unique identifier for the reason that's used internally. It's recommended to use only lowercase letters and underscores (`_`). For example, `pricing_error`.
- **Description**: Enter an optional description for the refund reason.
4. Once done, click the Save button.
![Create refund reason form](https://res.cloudinary.com/dza7lstvk/image/upload/v1759994670/User%20Guide/CleanShot_2025-10-09_at_10.24.22_2x_mmkr8z.png)
---
## Edit a Refund Reason
To edit a refund reason:
1. Go to Settings → Refund Reasons.
2. Find the refund reason to edit and click the <InlineIcon Icon={EllipsisHorizontal} alt="three-dots" /> icon in its row.
3. Choose Edit from the dropdown.
4. In the side window that opens, you can edit the refund reason's code, label, and description.
5. Once you're done, click the Save button.
![Edit refund reason form](https://res.cloudinary.com/dza7lstvk/image/upload/v1759994731/User%20Guide/CleanShot_2025-10-09_at_10.25.22_2x_hyo1j3.png)
---
## Delete a Refund Reason
<Note type="warning">
Deleting a refund reason is irreversible.
</Note>
To delete a refund reason:
1. Go to Settings → Refund Reasons.
2. Find the refund reason to delete and click the <InlineIcon Icon={EllipsisHorizontal} alt="three-dots" /> icon in its row.
3. Choose Delete from the dropdown.
4. Confirm deleting the refund reason by clicking the Delete button in the pop-up.

View File

@@ -18,6 +18,18 @@ To learn how to change the language, refer to the [Manage Profile](../../setting
---
## Right-to-Left (RTL) Language Support
<Note>
Right-to-left (RTL) language support is available from [Medusa Admin v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0). If your dashboard doesn't adjust to RTL languages, request your technical team to [upgrade your Medusa application](!docs!/learn/update).
</Note>
When you change the language to a right-to-left (RTL) language, such as Arabic or Hebrew, the Medusa Admin interface automatically adjusts to support RTL text direction. This includes aligning text and UI elements appropriately for better readability and user experience.
---
## Official Languages
- English

View File

@@ -1,11 +1,11 @@
export const generatedEditDates = {
"app/tips/languages/page.mdx": "2025-06-10T13:21:36.282Z",
"app/tips/languages/page.mdx": "2025-10-09T07:53:20.296Z",
"app/tips/bulk-editor/page.mdx": "2025-04-07T13:20:09.040Z",
"app/tips/lists/page.mdx": "2025-02-14T14:16:04.867Z",
"app/settings/sales-channels/page.mdx": "2025-05-30T13:31:09.754Z",
"app/settings/users/page.mdx": "2025-05-30T13:31:29.991Z",
"app/page.mdx": "2025-02-26T14:18:27.755Z",
"app/settings/page.mdx": "2025-08-28T13:59:06.712Z",
"app/settings/page.mdx": "2025-10-09T07:25:58.554Z",
"app/products/export/page.mdx": "2025-07-31T14:29:30.511Z",
"app/settings/return-reasons/page.mdx": "2025-05-30T13:31:05.596Z",
"app/settings/regions/page.mdx": "2025-05-30T13:30:59.228Z",
@@ -22,7 +22,7 @@ export const generatedEditDates = {
"app/inventory/inventory/page.mdx": "2025-05-30T13:27:17.017Z",
"app/customers/groups/page.mdx": "2025-05-30T13:26:56.820Z",
"app/orders/manage/page.mdx": "2025-09-09T06:46:05.439Z",
"app/orders/returns/page.mdx": "2025-05-30T13:28:28.116Z",
"app/orders/returns/page.mdx": "2025-10-09T07:33:10.189Z",
"app/inventory/page.mdx": "2025-05-30T13:27:31.179Z",
"app/orders/claims/page.mdx": "2025-05-30T13:27:39.540Z",
"app/orders/fulfillments/page.mdx": "2025-05-30T13:28:08.998Z",
@@ -31,7 +31,7 @@ export const generatedEditDates = {
"app/products/collections/page.mdx": "2025-05-30T13:29:05.948Z",
"app/customers/manage/page.mdx": "2025-07-31T14:28:07.889Z",
"app/discounts/create/page.mdx": "2024-05-03T17:36:38+03:00",
"app/orders/payments/page.mdx": "2025-08-26T09:54:18.417Z",
"app/orders/payments/page.mdx": "2025-10-09T07:31:59.781Z",
"app/discounts/page.mdx": "2024-05-03T17:36:38+03:00",
"app/orders/exchanges/page.mdx": "2025-05-30T13:27:55.646Z",
"app/products/create/page.mdx": "2025-05-30T13:29:24.876Z",
@@ -39,16 +39,16 @@ export const generatedEditDates = {
"app/products/variants/page.mdx": "2025-05-30T13:29:45.049Z",
"app/products/create/bundle/page.mdx": "2025-05-30T13:29:15.958Z",
"app/products/create/multi-part/page.mdx": "2025-05-30T13:29:20.321Z",
"app/promotions/campaigns/page.mdx": "2025-05-30T13:29:54.241Z",
"app/promotions/create/page.mdx": "2025-10-01T09:51:15.062Z",
"app/promotions/manage/page.mdx": "2025-05-30T13:30:02.678Z",
"app/promotions/campaigns/page.mdx": "2025-10-13T10:14:17.948Z",
"app/promotions/create/page.mdx": "2025-10-14T12:09:22.188Z",
"app/promotions/manage/page.mdx": "2025-10-14T12:09:22.188Z",
"app/promotions/page.mdx": "2025-05-30T13:30:08.538Z",
"app/price-lists/create/page.mdx": "2025-05-30T13:28:41.126Z",
"app/price-lists/manage/page.mdx": "2025-05-30T13:28:47.929Z",
"app/price-lists/page.mdx": "2025-05-30T13:28:53.668Z",
"app/settings/tax-regions/page.mdx": "2025-05-30T13:31:21.021Z",
"app/settings/locations-and-shipping/locations/page.mdx": "2025-08-28T14:06:20.266Z",
"app/settings/locations-and-shipping/page.mdx": "2025-08-28T14:01:34.185Z",
"app/settings/locations-and-shipping/locations/page.mdx": "2025-10-09T07:38:11.270Z",
"app/settings/locations-and-shipping/page.mdx": "2025-10-09T07:40:33.084Z",
"app/settings/locations-and-shipping/shipping-profiles/page.mdx": "2025-05-30T13:30:37.452Z",
"app/settings/product-tags/page.mdx": "2025-05-30T13:30:46.705Z",
"app/settings/product-types/page.mdx": "2025-05-30T13:30:50.623Z",
@@ -59,5 +59,6 @@ export const generatedEditDates = {
"app/orders/draft-orders/create/page.mdx": "2025-08-26T09:34:53.621Z",
"app/orders/draft-orders/manage/page.mdx": "2025-08-26T09:47:17.568Z",
"app/orders/draft-orders/page.mdx": "2025-08-26T09:31:53.784Z",
"app/settings/locations-and-shipping/shipping-option-types/page.mdx": "2025-08-28T14:03:13.999Z"
"app/settings/locations-and-shipping/shipping-option-types/page.mdx": "2025-08-28T14:03:13.999Z",
"app/settings/refund-reasons/page.mdx": "2025-10-09T07:29:52.837Z"
}

View File

@@ -443,6 +443,14 @@ export const generatedSidebars = [
"path": "/settings/return-reasons",
"children": []
},
{
"loaded": true,
"isPathHref": true,
"type": "link",
"title": "Refund Reasons",
"path": "/settings/refund-reasons",
"children": []
},
{
"loaded": true,
"isPathHref": true,

View File

@@ -273,6 +273,11 @@ export const sidebar = [
title: "Return Reasons",
path: "/settings/return-reasons",
},
{
type: "link",
title: "Refund Reasons",
path: "/settings/refund-reasons",
},
{
type: "link",
title: "Sales Channels",

View File

@@ -43,6 +43,10 @@ export const order = [
"title": "Manage Order Returns",
"path": "https://docs.medusajs.com/user-guide/orders/returns"
},
{
"title": "Manage Refund Reasons",
"path": "https://docs.medusajs.com/user-guide/settings/refund-reasons"
},
{
"title": "Manage Return Reasons",
"path": "https://docs.medusajs.com/user-guide/settings/return-reasons"

View File

@@ -179,6 +179,10 @@ export const userGuide = [
"title": "Manage Profile",
"path": "https://docs.medusajs.com/user-guide/settings/profile"
},
{
"title": "Manage Refund Reasons",
"path": "https://docs.medusajs.com/user-guide/settings/refund-reasons"
},
{
"title": "Manage Regions",
"path": "https://docs.medusajs.com/user-guide/settings/regions"